1. The introduction

Did you get a better understanding of Function Component after reading the complete guide to useEffect?

This time in the Writing Resilient Components article, learn what Resilient Components are and why Function Components can do it.

Summary of 2.

Lint or Prettier might be more concerned with code being elastic than code being Prettier.

Dan summarized four characteristics of elastic components:

  1. Do not block the data stream.
  2. Always be ready to render.
  3. Do not have singleton components.
  4. Isolate local state.

The above rules don’t just apply to React; they apply to all UI components.

Do not block the rendered data stream

By not blocking the data flow, I mean don’t localize the received parameters or make the component completely controlled.

With the Class Component syntax, it is not uncommon to store props to state during a given lifecycle because of the concept of a lifecycle. However, once props is solidified to state, the component is out of control:

class Button extends React.Component {
  state = {
    color: this.props.color
  };
  render() {
    const { color } = this.state; // πŸ”΄ `color` is stale!
    return <button className={"Button-"+color} >{this.props.children}</button>; }}Copy the code

When the component refreshes again, the props. Color changes, but the state.color does not. If you try to through other life cycle (componentWillReceiveProps or componentDidUpdate) to repair, the code becomes difficult to manage.

However, Function Component has no lifecycle concept, so there is no need to store props to state, just render:

Function Button({color, children}) {return (// βœ… 'color' is always fresh! <button className={"Button-" + color}>{children}</button> ); }Copy the code

If you need to process the props, you can use useMemo to cache the process and re-execute it only when the dependency changes:

const textColor = useMemo(
  (a)= > slowlyCalculateTextColor(color),
  [color] // βœ… Don’t recalculate until `color` changes
);
Copy the code

Do not block the stream of side effects

Making a request is a side effect, and if you make a request within a component, it would be nice to recalculate when the fetch parameter changes.

class SearchResults extends React.Component {
  state = {
    data: null
  };
  componentDidMount() {
    this.fetchResults();
  }
  componentDidUpdate(prevProps) {
    if(prevProps.query ! = =this.props.query) {
      // βœ… Refetch on change
      this.fetchResults();
    }
  }
  fetchResults() {
    const url = this.getFetchUrl();
    // Do the fetching...
  }
  getFetchUrl() {
    return "http://myapi/results? query" + this.props.query; // βœ… Updates are handled
  }
  render() {
    // ...}}Copy the code

To implement it as a Class Component, we need to extract the request function getFetchUrl and call it at componentDidMount and componentDidUpdate. Also note that getFetchUrl is not executed at componentDidUpdate if the state.query parameter has not changed.

This is a bad maintenance experience, and if state.currentPage is added to the number argument, you’re likely missing a state.currentPage judgment in componentDidUpdate.

If you use Function Component, you can use useCallback to wrap the fetch as a whole:

The author processed the original text without using useCallback.

function SearchResults({ query }) {
  const [data, setData] = useState(null);
  const [currentPage, setCurrentPage] = useState(0);

  const fetchResults = useCallback((a)= > {
    return "http://myapi/results? query" + query + "&page=" + currentPage;
  }, [currentPage, query]);

  useEffect((a)= > {
    const url = getFetchUrl();
    // Do the fetching...
  }, [getFetchUrl]); // βœ… Refetch on change

  // ...
}
Copy the code

Function Component treats props and state data equally, and can completely encapsulate the fetch logic and “update judgment” in a Function via useCallback, and add this Function as an overall dependency to useEffect. If you add another parameter in the future, you can simply modify the fetchResults function, and the eslint-plugin-react-hooks plugin can statically analyze for missing dependencies.

Function Component not only aggregates dependencies, but also solves the problem of statically analyzing dependencies caused by the Function judgment of Class Components scattered in multiple lifetimes.

Do not block your data flow because of performance optimization

Manually comparing optimizations to PureComponent and React.memo is not safe, as you might forget to compare functions:

class Button extends React.Component {
  shouldComponentUpdate(prevProps) {
    // πŸ”΄ Doesn't compare this.props. OnClick
    return this.props.color ! == prevProps.color; } render() {const onClick = this.props.onClick; // πŸ”΄ Doesn't reflect updates
    const textColor = slowlyCalculateTextColor(this.props.color);
    return (
      <button
        onClick={onClick}
        className={"Button-"+this.props.color+"Button-text-"+textColor}
      >
        {this.props.children}
      </button>); }}Copy the code

The code above is manually optimized for shouldComponentUpdate, but ignores the onClick argument, so while onClick really doesn’t change most of the time, the code isn’t buggy:

class MyForm extends React.Component {
  handleClick = (a)= > {
    // βœ… Always the same function
    // Do something
  };
  render() {
    return (
      <>
        <h1>Hello!</h1>
        <Button color="green" onClick={this.handleClick}>
          Press me
        </Button>
      </>); }}Copy the code

But once onClick is implemented in a different way, the situation is different, as in the following two cases:

class MyForm extends React.Component {
  state = {
    isEnabled: true
  };
  handleClick = (a)= > {
    this.setState({ isEnabled: false });
    // Do something
  };
  render() {
    return (
      <>
        <h1>Hello!</h1>
        <Button
          color="green"
          onClick={/ / πŸ”΄Button ignores updates to the onClick prop
            this.state.isEnabled ? this.handleClick : null
          }
        >
          Press me
        </Button>
      </>); }}Copy the code

OnClick toggles randomly between null and this.handleClick.

drafts.map(draft= > (
  <Button
    color="blue"
    key={draft.id}
    onClick={/ / πŸ”΄Button ignores updates to the onClick prop
      this.handlePublish.bind(this.draft.content)} >
    Publish
  </Button>
));
Copy the code

If draft.content changes, the onClick function changes.

That is, if the child component is manually optimized and the comparison of functions is missed, it is very likely that the old function will be executed, resulting in faulty logic.

In the Function Component environment, internally declared functions have different references each time, so it is easy to find logic bugs. Using useCallback and useContext can help solve this problem.

Be ready to render

Make sure your components can be rerendered at any time without causing internal state management bugs.

It can be difficult to do this. For example, if a complex component receives a state as a starting point, and subsequent code derives many internal states from that starting point, at some point the starting value changes, will the component still work?

For example:

// πŸ€” Should prevent unnecessary re-renders... right?
class TextInput extends React.PureComponent {
  state = {
    value: ""
  };
  // πŸ”΄ Resets local state on every parent render
  componentWillReceiveProps(nextProps) {
    this.setState({ value: nextProps.value });
  }
  handleChange = e= > {
    this.setState({ value: e.target.value });
  };
  render() {
    return <input value={this.state.value} onChange={this.handleChange} />; }}Copy the code

ComponentWillReceiveProps identifies each component receives the new props, props will be. The synchronous to the state. The value of the value. This is a derived state, and while it looks like it could gracefully handle changes to props, rerender of the parent element for some other reason causes the state.value to reset abnormally, such as forceUpdate of the parent element.

You can, of course, do this by not blocking the rendered stream, PureComponent, shouldComponentUpdate, react. memo for performance optimization (state.value is not reset if the props. Value is not changed), but such code is still fragile.

Robust code does not BUG just because an optimization is removed, and you can avoid this problem by not using derived state.

If the component relies on props. Value, then you don’t need to use state.value and make it a controlled component. 2. If there must be a state.value, make it internal, that is, don’t receive props. Avoid writing “in-between controlled and uncontrolled components” in general.

As a side note, if you want to reset the initial value of an uncontrolled component, add a key to the parent call:

<EmailInput defaultEmail={this.props.user.email} key={this.props.user.id} />
Copy the code

Alternatively, you can use ref to provide a reset function, but ref is not recommended.

Do not have singleton components

A flexible application should pass the following tests:

ReactDOM.render(
  <>
    <MyApp />
    <MyApp />
  </>,
  document.getElementById("root")
);
Copy the code

Render the entire application twice and see if it works separately?

In addition to the fact that component local state is maintained locally, resilient components should not “forever miss out on some state or functionality” because some function is called by another instance.

The authors add that a dangerous component is generally thought of as follows: no one is going to tamper with the data stream, so it’s just a matter of data initialization and destruction during didMount and unMount.

When another instance is destroyed, the intermediate state of that instance may be broken. A resilient Component should be able to respond to changes in state, and a Function Component without the concept of a life cycle is much easier to handle.

Quarantine local state

It is often difficult to determine whether data belongs to the local or global state of a component.

The article offers a way to determine: “Imagine that this component renders two instances at the same time. Does this data affect both instances? If the answer is no, then the data is appropriate as a local state.

Especially when writing business components, it is easy to confuse business data with the state data of the component itself.

In my experience, the number of local states increases from the top level of business to the bottom level of generic components:

Business -> Global data flow -> pages (completely dependent on the global data flow, with little state of their own) -> Business components (inheriting data from a page or global data flow, with little state of their own) -> Generic components (fully controlled, such as input; Or complex general logic for a large number of cohesive states, such as monaco-Editor)Copy the code

3. The intensive reading

Again, a resilient component needs to satisfy all four of the following principles:

  1. Do not block the data stream.
  2. Always be ready to render.
  3. Do not have singleton components.
  4. Isolate local state.

It may seem easy to follow these rules, but there are some problems in practice. Here are a few examples.

Pass callback functions frequently

Function Component leads to finer Component granularity, which improves maintainability while making global state a thing of the past. This code might make you feel uncomfortable:

const App = memo(function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("nick");

  return (
    <>
      <Count count={count} setCount={setCount}/>
      <Name name={name} setName={setName}/>
    </>
  );
});

const Count = memo(function Count(props) {
  return (
      <input value={props.count} onChange={pipeEvent(props.setCount)}>
  );
});

const Name = memo(function Name(props) {
  return (
  <input value={props.name} onChange={pipeEvent(props.setName)}>
  );
});
Copy the code

Although the logic is more decoupled by splitting the child components Count and Name, it becomes cumbersome for the child component to need to update the state of the parent component, and we do not want to pass functions through the child component as arguments.

One way to do this is to pass functions to child components through the Context:

const SetCount = createContext(null)
const SetName = createContext(null)

const App = memo(function App() {
  const [count, setCount] = useState(0);
  const [name, setName] = useState("nick");

  return (
    <SetCount.Provider value={setCount}>
      <SetName.Provider value={setName}>
        <Count count={count}/>
        <Name name={name}/>
      </SetName.Provider>
    </SetCount.Provider>
  );
});

const Count = memo(function Count(props) {
  const setCount = useContext(SetCount)
  return (
      <input value={props.count} onChange={pipeEvent(setCount)}>
  );
});

const Name = memo(function Name(props) {
  const setName = useContext(SetName)
  return (
  <input value={props.name} onChange={pipeEvent(setName)}>
  );
});
Copy the code

However, this will cause the Provider to be too bloated. Therefore, it is recommended that some components use useReducer instead of useState and merge functions into Dispatch:

const AppDispatch = createContext(null)

class State = {
  count = 0
  name = 'nick'
}

function appReducer(state, action) {
  switch(action.type) {
    case 'setCount':
      return {
        ...state,
        count: action.value
      }
    case 'setName':
      return {
        ...state,
        name: action.value
      }
    default:
      return state
  }
}

const App = memo(function App() {
  const [state, dispatch] = useReducer(appReducer, new State())

  return (
    <AppDispatch.Provider value={dispaych}>
      <Count count={count}/>
      <Name name={name}/>
    </AppDispatch.Provider>
  );
});

const Count = memo(function Count(props) {
  const dispatch = useContext(AppDispatch)
  return (
      <input value={props.count} onChange={pipeEvent(value => dispatch({type: 'setCount', value}))}>
  );
});

const Name = memo(function Name(props) {
  const dispatch = useContext(AppDispatch)
  return (
  <input value={props.name} onChange={pipeEvent(pipeEvent(value => dispatch({type: 'setName', value})))}>
  );
});
Copy the code

The state is aggregated into a Reducer so that a ContextProvider can solve all data processing problems.

The Memo wrapped components are like PureComponent effects.

The useCallback parameter changes frequently

In the close reading complete guide to useEffect we introduced how to create an Immutable function using useCallback:

function Form() {
  const [text, updateText] = useState("");

  const handleSubmit = useCallback((a)= > {
    const currentText = text;
    alert(currentText);
  }, [text]);

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}
Copy the code

But the function’s dependency [text] changes so frequently that the handleSubmit function is regenerated in every render, which has a performance impact. One solution is to circumvent this problem with Ref:

function Form() {
  const [text, updateText] = useState("");
  const textRef = useRef();

  useEffect((a)= > {
    textRef.current = text; // Write it to the ref
  });

  const handleSubmit = useCallback((a)= > {
    const currentText = textRef.current; // Read it from the ref
    alert(currentText);
  }, [textRef]); // Don't recreate handleSubmit like [text] would do

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}
Copy the code

Of course, you can also wrap this procedure as a custom Hooks to make the code look a little better:

function Form() {
  const [text, updateText] = useState("");
  // Will be memoized even if `text` changes:
  const handleSubmit = useEventCallback((a)= > {
    alert(text);
  }, [text]);

  return (
    <>
      <input value={text} onChange={e => updateText(e.target.value)} />
      <ExpensiveTree onSubmit={handleSubmit} />
    </>
  );
}

function useEventCallback(fn, dependencies) {
  const ref = useRef(() => {
    throw new Error("Cannot call an event handler while rendering.");
  });

  useEffect(() => {
    ref.current = fn;
  }, [fn, ...dependencies]);

  return useCallback(() => {
    const fn = ref.current;
    return fn();
  }, [ref]);
}
Copy the code

However, this solution is not elegant, and React is considering a more elegant solution.

A potentially abusive useReducer

As mentioned in the section “decouple updates from actions” in the close reading complete guide to useEffect, use useReducer to solve “the problem of functions relying on multiple external variables at the same time”.

In general, we use useReducer like this:

const reducer = (state, action) => { switch (action.type) { case "increment": return { value: state.value + 1 }; case "decrement": return { value: state.value - 1 }; case "incrementAmount": return { value: state.value + action.amount }; default: throw new Error(); }}; const [state, dispatch] = useReducer(reducer, { value: 0 });Copy the code

In fact, useReducer can define state and action arbitrarily, so we can use useReducer to create a useState.

For example, we create a useState with a plural key:

const [state, setState] = useState({ count: 0.name: "nick" });

/ / modify the count
setState(state= > ({ ...state, count: 1 }));

/ / modify the name
setState(state= > ({ ...state, name: "jack" }));
Copy the code

Use useReducer to implement similar functions:

function reducer(state, action) {
  return action(state);
}

const [state, dispatch] = useReducer(reducer, { count: 0.name: "nick" });

/ / modify the count
dispatch(state= > ({ ...state, count: 1 }));

/ / modify the name
dispatch(state= > ({ ...state, name: "jack" }));
Copy the code

Therefore, in view of the above situation, we may abuse useReducer and suggest using useState directly instead.

4. To summarize

This article summarizes four characteristics of resilient components: don’t block data flow, always render ready, don’t have singleton components, and isolate local state.

This convention is important for code quality and is not easily recognized by Lint rules or simple visual observations, so it is difficult to generalize.

Overall, Function Component brings a more elegant code experience, but also a higher level of teamwork.

The discussion address is: Close reading writing Flexible Components Β· Issue #139 Β· dT-fe /weekly

If you’d like to participate in the discussion, pleaseClick here to, with a new theme every week, released on weekends or Mondays. Front end Intensive Reading – Helps you filter the right content.

Pay attention to the front end of intensive reading wechat public account

special Sponsors

  • DevOps full process platform

Copyright Notice: Freely reproduced – Non-commercial – Non-derivative – Remain signed (Creative Commons 3.0 License)