WHAT - React 学习系列(六)- Managing state

Overview

As your application grows, it helps to be more intentional about how your state is organized and how the data flows between your components.

In this chapter, you’ll learn how to structure your state well, how to keep your state update logic maintainable, and how to share state between distant components.

Reacting to input with state
With React, you won’t modify the UI from code directly. For example, you won’t write commands like “disable the button”, “enable the button”, “show the success message”, etc. Instead, you will describe the UI you want to see for the different visual states of your component (“initial state”, “typing state”, “success state”), and then trigger the state changes in response to user input.

Choosing the state structure
The most important principle is that state shouldn’t contain redundant or duplicated information. If there’s unnecessary state, it’s easy to forget to update it, and introduce bugs!

For example, this form has a redundant fullName state variable:

  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const [fullName, setFullName] = useState('');

You can remove it and simplify the code by calculating fullName while the component is rendering:

  const [firstName, setFirstName] = useState('');
  const [lastName, setLastName] = useState('');
  const fullName = firstName + ' ' + lastName;

This might seem like a small change, but many bugs in React apps are fixed this way.

Sharing state between components & lifting state up
Sometimes, you want the state of two components to always change together. To do it, remove state from both of them, move it to their closest common parent, and then pass it down to them via props.
This is known as “lifting state up”, and it’s one of the most common things you will do writing React code.

Preserving and resetting state
When you re-render a component, React needs to decide which parts of the tree to keep (and update), and which parts to discard or re-create from scratch.
By default, React preserves the parts of the tree that “match up” with the previously rendered component tree.
React lets you override the default behavior, and force a component to reset its state by passing it a different key. This tells React that if the recipient is different, it should be considered a different Chat component that needs to be re-created from scratch with the new data (and UI like inputs).

const contacts = [
  { name: 'Taylor', email: 'taylor@mail.com' },
  { name: 'Alice', email: 'alice@mail.com' },
  { name: 'Bob', email: 'bob@mail.com' }
];
const [to, setTo] = useState(contacts[0]);

<Chat contact={to} />: Typing a message and then switching the recipient does not reset the input
<Chat key={to.email} contact={to} />: Now switching between the recipients resets the input field—even though you render the same component.

Extracting state logic into a reducer
Components with many state updates spread across many event handlers can get overwhelming.
For these cases, you can consolidate all the state update logic outside your component in a single function, called “reducer”. Your event handlers become concise because they only specify the user “actions”

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}
const initialTasks = [
  { id: 0, text: 'Visit Kafka Museum', done: true },
  { id: 1, text: 'Watch a puppet show', done: false },
  { id: 2, text: 'Lennon Wall pic', done: false }
];

export default function TaskApp() {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );
  function handleAddTask(text) {
    dispatch({
      type: 'added',
      id: nextId++,
      text: text,
    });
  }
  function handleChangeTask(task) {
    dispatch({
      type: 'changed',
      task: task
    });
  }
  function handleDeleteTask(taskId) {
    dispatch({
      type: 'deleted',
      id: taskId
    });
  }
}

Passing data deeply with context
Usually, you will pass information from a parent component to a child component via props. But passing props can become inconvenient if you need to pass some prop through many components, or if many components need the same information.
Context lets the parent component make some information available to any component in the tree below it—no matter how deep it is—without passing it explicitly through props.

Scaling up with reducer and context
Reducers let you consolidate a component’s state update logic. Context lets you pass information deep down to other components. You can combine reducers and context together to manage state of a complex screen.
With this approach, a parent component with complex state manages it with a reducer. Other components anywhere deep in the tree can read its state via context. They can also dispatch actions to update that state.

Reacting to input with state

https://react.dev/learn/reacting-to-input-with-state

How declarative UI compares to imperative?
In React, you don’t directly manipulate the UI—meaning you don’t enable, disable, show, or hide components directly. Instead, you declare what you want to show, and React figures out how to update the UI. Think of getting into a taxi and telling the driver where you want to go instead of telling them exactly where to turn. It’s the driver’s job to get you there, and they might even know some shortcuts you haven’t considered!

UI declaratively?
You’ve seen how to implement a form imperatively above. To better understand how to think in React, you’ll walk through reimplementing this UI in React below:

  1. Identify your component’s different visual states
  2. Determine what triggers those state changes
  3. Represent the state in memory using useState
  4. Remove any non-essential state variables
  5. Connect the event handlers to set the state

Step 1: Identify your component’s different visual states
In computer science, you may hear about a “state machine” being in one of several “states”. If you work with a designer, you may have seen mockups for different “visual states”. React stands at the intersection of design and computer science, so both of these ideas are sources of inspiration.
https://en.wikipedia.org/wiki/Finite-state_machine

Step 2: Determine what triggers those state changes
You can trigger state updates in response to two kinds of inputs:

  • Human inputs, like clicking a button, typing in a field, navigating a link.
  • Computer inputs, like a network response arriving, a timeout completing, an image loading.
    In both cases, you must set state variables to update the UI.

Notice that human inputs often require event handlers!

Step 3: Represent the state in memory with useState
Next you’ll need to represent the visual states of your component in memory with useState. Simplicity is key: each piece of state is a “moving piece”, and you want as few “moving pieces” as possible. More complexity leads to more bugs!

Step 4: Remove any non-essential state variables
You want to avoid duplication in the state content so you’re only tracking what is essential.

Spending a little time on refactoring your state structure will make your components easier to understand, reduce duplication, and avoid unintended meanings.

Your goal is to prevent the cases where the state in memory doesn’t represent any valid UI that you’d want a user to see. (For example, you never want to show an error message and disable the input at the same time, or the user won’t be able to correct the error!)

Here are some questions you can ask about your state variables:

  • Does this state cause a paradox?
  • Is the same information available in another state variable already?
  • Can you get the same information from the inverse of another state variable?

Eliminating “impossible” states with a reducer?

const [answer, setAnswer] = useState('');
const [error, setError] = useState(null);
const [status, setStatus] = useState('typing'); // 'typing', 'submitting', or 'success'

These three variables are a good enough representation of this form’s state. However, there are still some intermediate states that don’t fully make sense. For example, a non-null error doesn’t make sense when status is ‘success’.
To model the state more precisely, you can extract it into a reducer. Reducers let you unify multiple state variables into a single object and consolidate all the related logic!
https://react.dev/learn/extracting-state-logic-into-a-reducer

import React, { useReducer } from 'react';

// Initial state
const initialState = {
  answer: '',
  error: null,
  status: 'typing' // 'typing', 'submitting', or 'success'
};

// Reducer function
const formReducer = (state, action) => {
  switch (action.type) {
    case 'SET_ANSWER':
      return {
        ...state,
        answer: action.payload,
        error: null  // Clear error when answer changes
      };
    case 'SET_ERROR':
      return {
        ...state,
        error: action.payload,
        status: 'typing' // Reset status to 'typing' when error occurs
      };
    case 'SET_STATUS':
      return {
        ...state,
        status: action.payload
      };
    case 'RESET_FORM':
      return initialState; // Reset form to initial state
    default:
      return state;
  }
};

function Form() {
  const [state, dispatch] = useReducer(formReducer, initialState);
  const { answer, error, status } = state;

  // Handler to update answer
  const handleAnswerChange = (e) => {
    dispatch({ type: 'SET_ANSWER', payload: e.target.value });
  };

  // Handler to submit form
  const handleSubmit = async () => {
    try {
      dispatch({ type: 'SET_STATUS', payload: 'submitting' });

      // Simulate API call or validation logic
      if (answer.trim() === '') {
        throw new Error('Answer cannot be empty');
      }

      // Successful submission
      dispatch({ type: 'SET_STATUS', payload: 'success' });
    } catch (err) {
      dispatch({ type: 'SET_ERROR', payload: err.message });
    }
  };

  return (
    <div>
      <input
        type="text"
        value={answer}
        onChange={handleAnswerChange}
        placeholder="Type your answer..."
      />
      {error && <p style={{ color: 'red' }}>{error}</p>}
      {status === 'typing' && <button onClick={handleSubmit}>Submit</button>}
      {status === 'submitting' && <p>Submitting...</p>}
      {status === 'success' && <p style={{ color: 'green' }}>Submitted successfully!</p>}
    </div>
  );
}

export default Form;

Step 5: Connect the event handlers to set state
Lastly, create event handlers that update the state.

Expressing all interactions as state changes lets you later introduce new visual states without breaking existing ones. It also lets you change what should be displayed in each state without changing the logic of the interaction itself.

Choosing the state structure

Here are some tips you should consider when structuring state.

When you write a component that holds some state, you’ll have to make choices about how many state variables to use and what the shape of their data should be. There are a few principles that can guide you to make better choices:

  1. Group related state. If you always update two or more state variables at the same time, consider merging them into a single state variable. Another case where you’ll group data into an object or an array is when you don’t know how many pieces of state you’ll need. For example, it’s helpful when you have a form where the user can add custom fields.
  2. Avoid contradictions in state. When the state is structured in a way that several pieces of state may contradict and “disagree” with each other, you leave room for mistakes. Try to avoid this.
  3. Avoid redundant state. If you can calculate some information from the component’s props or its existing state variables during rendering, you should not put that information into that component’s state. And don’t mirror props in state. https://react.dev/learn/choosing-the-state-structure#don-t-mirror-props-in-state “Mirroring” props into state only makes sense when you want to ignore all updates for a specific prop. By convention, start the prop name with initial or default to clarify that its new values are ignored
  4. Avoid duplication in state. When the same data is duplicated between multiple state variables, or within nested objects, it is difficult to keep them in sync. Reduce duplication when you can. You didn’t need to hold the selected item in state, because only the selected ID is essential.
  5. Avoid deeply nested state. Deeply hierarchical state is not very convenient to update. When possible, prefer to structure state in a flat way.
    The goal behind these principles is to make state easy to update without introducing mistakes.

https://react.dev/learn/choosing-the-state-structure 可以查看相关代码示例

Avoid deeply nested state?
Updating nested state involves making copies of objects all the way up from the part that changed. Deleting a deeply nested place would involve copying its entire parent place chain. Such code can be very verbose.
If the state is too nested to update easily, consider making it “flat”. Here is one way you can restructure this data. Instead of a tree-like structure where each place has an array of its child places, you can have each place hold an array of its child place IDs. Then store a mapping from each place ID to the corresponding place.

export const initialTravelPlan = {
  0: {
    id: 0,
    title: '(Root)',
    childIds: [1, 42, 46],
  },
  1: {
    id: 1,
    title: 'Earth',
    childIds: [2, 10, 19, 26, 34]
  },const [plan, setPlan] = useState(initialTravelPlan);

You can nest state as much as you like, but making it “flat” can solve numerous problems. It makes state easier to update, and it helps ensure you don’t have duplication in different parts of a nested object.

Sharing state between components & lifting state up

Sometimes, you want the state of two components to always change together. To do it, remove state from both of them, move it to their closest common parent, and then pass it down to them via props. This is known as lifting state up, and it’s one of the most common things you will do writing React code.

Controlled and uncontrolled components?
https://react.dev/learn/sharing-state-between-components#controlled-and-uncontrolled-components
It is common to call a component with some local state “uncontrolled”. For example, the original Panel component with an isActive state variable is uncontrolled because its parent cannot influence whether the panel is active or not. In contrast, you might say a component is “controlled” when the important information in it is driven by props rather than its own local state.
Uncontrolled components are easier to use within their parents because they require less configuration. But they’re less flexible when you want to coordinate them together. Controlled components are maximally flexible, but they require the parent components to fully configure them with props.
In practice, “controlled” and “uncontrolled” aren’t strict technical terms—each component usually has some mix of both local state and props. However, this is a useful way to talk about how components are designed and what capabilities they offer.
When writing a component, consider which information in it should be controlled (via props), and which information should be uncontrolled (via state). But you can always change your mind and refactor later.

For each unique piece of state, you will choose the component that “owns” it. This principle is also known as having a “single source of truth”.
https://en.wikipedia.org/wiki/Single_source_of_truth

Your app will change as you work on it. It is common that you will move state down or back up while you’re still figuring out where each piece of the state “lives”. This is all part of the process!

Preserving and resetting state

State is isolated between components. React keeps track of which state belongs to which component based on their place in the UI tree. You can control when to preserve state and when to reset it between re-renders.

React builds render trees for the component structure in your UI.
https://react.dev/learn/understanding-your-ui-as-a-tree#the-render-tree

When you give a component state, you might think the state “lives” inside the component. But the state is actually held inside React. React associates each piece of state it’s holding with the correct component by where that component sits in the render tree.

In React, each component on the screen has fully isolated state. or example, if you render two Counter components side by side, each of them will get its own, independent states.

React preserves a component’s state for as long as it’s being rendered at its position in the UI tree. If it gets removed, or a different component gets rendered at the same position, React discards its state.

Remember that it’s the position in the UI tree—not in the JSX markup—that matters to React!

      {isFancy ? (
        <Counter isFancy={true} /> 
      ) : (
        <Counter isFancy={false} /> 
      )}

In this example, there are two different tags, Whether isFancy is true or false, you always have a as the first child of the div returned from the root App component. It’s the same component at the same position, so from React’s perspective, it’s the same counter.

React doesn’t know where you place the conditions in your function. All it “sees” is the tree you return.

But when you render a different component in the same position, it resets the state of its entire subtree.

      {isFancy ? (
        <div>
          <Counter isFancy={true} /> 
        </div>
      ) : (
        <section>
          <Counter isFancy={false} />
        </section>
      )}

The counter state gets reset when you click the checkbox.

If you want to preserve the state between re-renders, the structure of your tree needs to “match up” from one render to another. If the structure is different, the state gets destroyed because React destroys state when it removes a component from the tree.

How to reset state at the same position?
By default, React preserves state of a component while it stays at the same position. Usually, this is exactly what you want, so it makes sense as the default behavior. But sometimes, you may want to reset a component’s state.
There are two ways to reset state when switching between them:

  1. Render components in different positions
  2. Give each component an explicit identity with key
      {isPlayerA ? (
        <Counter key="Taylor" person="Taylor" />
      ) : (
        <Counter key="Sarah" person="Sarah" />
      )}

Remember that keys are not globally unique. They only specify the position within the parent. Resetting state with a key is particularly useful when dealing with forms.
https://react.dev/learn/preserving-and-resetting-state#resetting-a-form-with-a-key

Preserving state for removed components?
In a real chat app, you’d probably want to recover the input state when the user selects the previous recipient again. There are a few ways to keep the state “alive” for a component that’s no longer visible:

  • You could render all chats instead of just the current one, but hide all the others with CSS.
  • You could lift the state up and hold the pending message for each recipient in the parent component.
  • You might also use a different source in addition to React state.

Extracting state logic into a reducer

https://react.dev/learn/extracting-state-logic-into-a-reducer

Components with many state updates spread across many event handlers can get overwhelming.

For these cases, you can consolidate all the state update logic outside your component in a single function, called a reducer.

consolidate?
To reduce this complexity and keep all your logic in one easy-to-access place, you can move that state logic into a single function outside your component, called a “reducer”.
Reducers are a different way to handle state. You can migrate from useState to useReducer in three steps:

  1. Move from setting state to dispatching actions.
  2. Write a reducer function.
  3. Use the reducer from your component.

It’s a convention to use switch statements inside reducers. The result is the same, but it can be easier to read switch statements at a glance.

We recommend wrapping each case block into the { and } curly braces so that variables declared inside of different cases don’t clash with each other. Also, a case should usually end with a return.

The useReducer Hook takes two arguments:

  1. A reducer function
  2. An initial state
    And it returns:
  3. A stateful value
  4. A dispatch function (to “dispatch” user actions to the reducer)

If you want, you can even move the reducer to a different file.

Comparing useState and useReducer
https://react.dev/learn/extracting-state-logic-into-a-reducer#comparing-usestate-and-usereducer

Writing reducers well?
Keep these two tips in mind when writing reducers:

  1. Reducers must be pure.
  2. Each action describes a single user interaction, even if that leads to multiple changes in the data.
    https://react.dev/learn/extracting-state-logic-into-a-reducer#writing-reducers-well

Writing concise reducers with Immer
https://react.dev/learn/extracting-state-logic-into-a-reducer#writing-concise-reducers-with-immer
Just like with updating objects and arrays in regular state, you can use the Immer library to make reducers more concise.
Reducers must be pure, so they shouldn’t mutate state. But Immer provides you with a special draft object which is safe to mutate. Under the hood, Immer will create a copy of your state with the changes you made to the draft. This is why reducers managed by useImmerReducer can mutate their first argument and don’t need to return state.

Passing data deeply with context

Context lets the parent component make some information available to any component in the tree below it—no matter how deep—without passing it explicitly through props.

  1. Create a context. (You can call it LevelContext, since it’s for the heading level.)
  2. Use that context from the component that needs the data. (Heading will use LevelContext.)
  3. Provide that context from the component that specifies the data. (Section will provide LevelContext.)

First, you need to create the context. You’ll need to export it from a file so that your components can use it:

// LevelContext.js
import { createContext } from 'react';
export const LevelContext = createContext(1);

Import the useContext Hook from React and your context:

// Heading.js
import { useContext } from 'react';
import { LevelContext } from './LevelContext.js';
export default function Heading({ children }) {
  const level = useContext(LevelContext);
  // ...
}

useContext tells React that the Heading component wants to read the LevelContext.

Wrap them with a context provider to provide the LevelContext to them:

// Section.js
import { LevelContext } from './LevelContext.js';

export default function Section({ level, children }) {
  return (
    <section className="section">
      <LevelContext.Provider value={level}>
        {children}
      </LevelContext.Provider>
    </section>
  );
}

This tells React: “if any component inside this

asks for LevelContext, give them this level.”

You can pass down any information needed by the entire subtree: the current color theme, the currently logged in user, and so on.

  1. Theming
  2. Current account
  3. Routing
  4. Managing state
    https://react.dev/learn/passing-data-deeply-with-context#use-cases-for-context

Using and providing context from the same component
https://react.dev/learn/passing-data-deeply-with-context#using-and-providing-context-from-the-same-component

Context passes through intermediate components
https://react.dev/learn/passing-data-deeply-with-context#context-passes-through-intermediate-components
How context works might remind you of CSS property inheritance. In CSS, you can specify color: blue for a <div>, and any DOM node inside of it, no matter how deep, will inherit that color unless some other DOM node in the middle overrides it with color: green. Similarly, in React, the only way to override some context coming from above is to wrap children into a context provider with a different value.
In CSS, different properties like color and background-color don’t override each other. You can set all <div>’s color to red without impacting background-color. Similarly, different React contexts don’t override each other. Each context that you make with createContext() is completely separate from other ones, and ties together components using and providing that particular context. One component may use or provide many different contexts without a problem.

Context is very tempting to use! However, this also means it’s too easy to overuse it.
Here’s a few alternatives you should consider before using context:

  1. Start by passing props.
  2. Extract components and pass JSX as children to them. For example, maybe you pass data props like posts to visual components that don’t use them directly, like<Layout posts={posts} />. Instead, make Layout take children as a prop, and render <Layout><Posts posts={posts} /></Layout>. This reduces the number of layers between the component specifying the data and the one that needs it.
    https://react.dev/learn/passing-data-deeply-with-context#before-you-use-context
    If neither of these approaches works well for you, consider context.

Scaling up with reducer and context

Reducers let you consolidate a component’s state update logic. Context lets you pass information deep down to other components. You can combine reducers and context together to manage state of a complex screen.

A reducer helps keep the event handlers short and concise. However, as your app grows, you might run into another difficulty. Currently, the tasks state and the dispatch function are only available in the top-level TaskApp component. To let other components read the list of tasks or change it, you have to explicitly pass down the current state and the event handlers that change it as props.

Here is how you can combine a reducer with context:

  1. Create the context.
  2. Put state and dispatch into context.
  3. Use context anywhere in the tree.
    https://react.dev/learn/scaling-up-with-reducer-and-context
// TasksContext.js
import { createContext, useReducer } from 'react';

export const TasksContext = createContext(null);
export const TasksDispatchContext = createContext(null);

export function TasksProvider({ children }) {
  const [tasks, dispatch] = useReducer(
    tasksReducer,
    initialTasks
  );

  return (
    <TasksContext.Provider value={tasks}>
      <TasksDispatchContext.Provider value={dispatch}>
        {children}
      </TasksDispatchContext.Provider>
    </TasksContext.Provider>
  );
}

function tasksReducer(tasks, action) {
  switch (action.type) {
    case 'added': {
      return [...tasks, {
        id: action.id,
        text: action.text,
        done: false
      }];
    }
    case 'changed': {
      return tasks.map(t => {
        if (t.id === action.task.id) {
          return action.task;
        } else {
          return t;
        }
      });
    }
    case 'deleted': {
      return tasks.filter(t => t.id !== action.id);
    }
    default: {
      throw Error('Unknown action: ' + action.type);
    }
  }
}

const initialTasks = [
  { id: 0, text: 'Philosopher’s Path', done: true },
  { id: 1, text: 'Visit the temple', done: false },
  { id: 2, text: 'Drink matcha', done: false }
];

// App.js
import AddTask from './AddTask.js';
import TaskList from './TaskList.js';
import { TasksProvider } from './TasksContext.js';

export default function TaskApp() {
  return (
    <TasksProvider>
      <h1>Day off in Kyoto</h1>
      <AddTask />
      <TaskList />
    </TasksProvider>
  );
}

As your app grows, you may have many context-reducer pairs like this. This is a powerful way to scale your app and lift state up without too much work whenever you want to access the data deep in the tree.

  • 33
    点赞
  • 13
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值