Learning Notes From New React Website

It has been a while after React published its new webiste, I would write down what I learnt from the new webiste.

Describe the UI

List Key

I know this is an old issue, but I used to use index as the key of list. To summarize it, I will note the correct way to use the key:

  • Why do we have to use key for the list item? Because the item in the list could be modified, such as insert, deleted or re-sorted, so the React need to know which item the component is responible to.
  • Why do we cannot use index as the key? If the list is not changed, it would be fine to use index as the key. But if we have the modificaiton operation to the list, like add, insert, delete or resort, React would be confused.
  • Why do we cannot use a random (non-duplicated) number as the key? Because with random new generated key, it would violate the reason React uses key. React would not know the item components it used to render and has to recreate all the related components and DOMs, which is not only slow, but also lose all the user inputs.

So the principles to use key are:

  1. It should be unique within the list locally, unnecessary to be globally
  2. Use the data which is unique naturally
  3. Do not change the key value accross rendering, it should be persistant.

Think in React with lifecycle

Actually, I used to have a question: why the local variable inside of a component would not be updated when I changed it, like this:

 1export default function Gallery() {
 2  let index = 0;
 3
 4  function handleClick() {
 5    index = index + 1;
 6  }
 7
 8  let sculpture = sculptureList[index];
 9  return (
10    <>
11      <button onClick={handleClick}>
12        Next
13      </button>
14      <h3>  
15        ({index + 1} of {sculptureList.length})
16      </h3>
17      <p>
18        {sculpture.description}
19      </p>
20    </>
21  );
22}

When we click on the button, the variable sculpture is not updated based on the index. So why? The reason for this is I only consider the native javascript, not taking one point knowledge of the frontend development into account. This knowledge is the rendering. React will render the component to the DOM and it uses the hooks to save the internal variables or states among the renderings. So either this local variable index will be recreated during each rendering which causes the click action useless, or updating the index value would not trigger the rendering which may reflect the updated value to the DOM.

So although we understand this, where does React save the internal states? Actually, React just save all the hooks in an internal array, and all the hooks would be identified by its index value. So this is also why the hooks can only be defined in the top level of component and cannot be used conditionally. Because if we do that, React would not be able to find the previous correct hooks.

Then we would rethink the useState hook. The second setter method returned by this hook will theoritically trigger the re-rendering. Everytime it's called, it will inform React to re-render the page and read the state value.

Batching

Batching means the UI will not be updated until the event handler, with codes inside of it, finishes. So if there are multiple setter methods, before the next re-render, all of them will be executed. This is a kind of 'old' new feature, it used to happen that the following setter methods are not executed in React's old version.

Updating the state in Object or Array

Since Object and array are the reference, not plain variable type, so React would not recognise the changing of the state if you just mutate the member values of them. The correct solution is to create a new one instead of passing the old one to setter method.

Position matters

Check this official example first

We might expect the state to be resetted when we check/uncheck the Use fancy styling checkbox. But it isn't. This is because both of these <Counter /> tags are rendered at the same position. Since this two Counter has the same structure, so React treat them as the same Counter.

Do remember React's explanation:

React will keep the state around for as long as you render the same component at the same position.

React preserves a component’s state for as long as it’s being rendered at its position in the UI tree.

Different components in the same position

But if there are different components in the same position, React will reset the state of the whole subtree.

Reset state in the same position

So how to distinguish the component if we have the same components with different props in the same position? For example, we have a video, and want to change its source url when click one button:

The expect behaviour is when we click on the button, video player auto plays the different videos. But it does not happen because the above reason, React applies the same props to the component in the same position. So even though the value isPlayingOne changes, the player does not reload.

One imperative solution would be adding a reference to the video element and a useEffect function to the Video component, when the src url changes, we reload the player. Check the code of file ImperativeVideo.tsx.

According to the solutions from React official website, we also have two declarative solutions:

One is to render the component in the different positions:

 1<h2>Video with different position</h2>
 2{isPlayingOne && (
 3  <OriginalVideo
 4    src={
 5      "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
 6    }
 7  />
 8)}
 9{!isPlayingOne && (
10  <OriginalVideo
11    src={
12      "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
13    }
14  />
15)}

The other would be assigning the key to the component, React would recognise the component with the key, check the file VideoWithKey.tsx. The key is only required to be unique within the parent, not globally.

1<Video
2  key={isPlayingOne ? "one" : "two"} // NOTE: Add this line
3  src={
4    isPlayingOne
5      ? "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/BigBuckBunny.mp4"
6      : "http://commondatastorage.googleapis.com/gtv-videos-bucket/sample/ElephantsDream.mp4"
7  }
8/>

Context -- sharing state with deep children

Normally, we do not use Context that often, only on the top level to pass the theme and Redux's store. One of the reason is it would not be testable for the component and using Context would make the props unpredictable. Actually it's very good to make it clear that the props passed to the component, especially for the person who maintain this explicit data flow component.

And according to the official doc, Context can also be avoided by passing the components as children. Like instead of using <Layout posts={posts} />, we can make Layout to accept the component as children, <Layout><Posts posts={posts} /></Layout>.

About Effect

The main principle of useEffect is it should mainly be used to specify the side effect caused by the rendering or external system, like fetching. There are several things we should not use Effect to do:

  1. Do not use Effect to calculate data from props and state and call the setter method. The reason for this is it introduces the unnecessary procedure to re-render the page. When the page gets rendered, after it finishes, it will trigger the useEffect method, as we called the setter method in the Effect. With setter method, the page will be re-rendered again.

  2. Cache the expensive data. use useMemo to cache the expensive data instead of calculating every render.

  3. Mostly do not update state according to props change. When you need to do this, re-think of your component in these ways:

    • compute the entire state based on the current state or props, use useMemo when too complicated
    • reset the entire component with using key
    • update the state in the related event handler However, in the rare case, it may still need to update the state from the rendering. Then be careful and remember to update it without in the Effect. Like this official example:
     1function CountLabel({ count }) {
     2  const [prevCount, setPrevCount] = useState(count);
     3  const [trend, setTrend] = useState(null);
     4  if (prevCount !== count) {
     5    setPrevCount(count);
     6    setTrend(count > prevCount ? 'increasing' : 'decreasing');
     7  }
     8  return (
     9    <>
    10      <h1>{count}</h1>
    11      {trend && <p>The count is {trend}</p>}
    12    </>
    13  );
    14}
    
  4. Use Effects only for code that should run because the component was displayed to the user, not user's behaviour. If a logic is shared, instead of creating Effect, think of creating a function for it.

  5. use useSyncExternalStore for subscribing to an external store.

  6. Only use the reactive variables as dependency list. All the variables inside of the component, including props, states and generated from props and states are reactive, because they are calculated during the rendering and participate in the React data flow. React requires to add these reactive variables in the Effects' dependency list. But the global and mutable values like, location.pathname, ref.current could not be dependencies. Because changing these mutable variables could happen outside of the component and it would not trigger a re-render

Difference between event handlers and Effects

  1. Event handler reacts to user's specific actions, while Effects runs because synchronization is required;
  2. Logic inside Event handlers is not reactive, which is mostly triggered by user, while logic inside Effects is reactive since something side effects should happen according to certain reactive values such as props, state changed;

Start question to use Effects

  1. Should this code be moved to event handler or be an Effect?
  2. Is the Effect doing several unrelated things? Several things should splitted to different Effects.
  3. Does some reactive value change unintentionally?
  4. Are you reading some state to calculate the next state?
    1. Move static objects and functions outside your component
    2. Move dynamic objects and functions inside your Effect
    3. Read primitive values from objects
    4. Calculate primitive values from functions

useEvent -- an event handler allowing side effects

React proposed a RFC hook useEvent to improve the way of using event handler, even though it's not official published yet, we have already use it generally in our codes.

To understand the motivation of useEvent, you can check the official explanation or this blog. But here I want to explain the usage of useEvent as event dispatcher and handler.

 1import { useMutation } from 'react-query'
 2
 3const addMutation = useMutation()
 4const [internalState, setInternalState] = useState({ contact: 'name' })
 5
 6const dispatch = useEvent((event) => eventHandler({event, state: internalState, setState: setInternalState, sideEffects: { addMutation }}))
 7
 8const eventHandler = ({ event, state, setState, sideEffects: { addMutation }}) => {
 9
10  switch(event) {
11    case 'add':
12      addMutation.mutate(state.contact, {
13        onSuccess: (succcess) => {
14          setState((prevState) => ({
15            ...prevState,
16            success,
17          }))
18        }
19      })
20      break;
21    default:
22      history.push('/test')
23  }
24}
25
26return (
27  <button onClick={() => dispatch('add')}>
28)

So from the above code, I defined a event handler called eventHandler which takes the parameter event from the function passed to the useEvent, the defined variables from useState and the last one sideEffects which I will explain it later. So I named the value returned by the useEvent dispatch, it can be called anywhere except in the rendering. In the codes, I called it when we click on the button.

In the defined eventHandler, I check the event type, when it's 'add', I will call the addMutation passed from the parameter sideEffects, and set the state once it succeeds. In the default branch, I want to change the route to the '/test'.

So the good thing of using this event handler with useEvent is it moves all the updating logic and side effects operations to the independent method eventHandler, so it's more clear to understand the modification operations since they are moved together and easier to understand. Besides it makes both the component and handler pure and testable. We can of course move this eventHandler to a single file.

Why not useReducer?

Yes, it comes a question automatically: why don't we use another state dispatcher and handler hook useState, since that seems simpler, at least it's not required to explicitly define the state, pass both state and setter method to the event handler.

1const [state, dispatch] = useReducer(reducer, initialArg, init)

The quick answer is useReducer cannot handle the side effect, since its reducer has to be pure, and return the updated state every time, while eventHandler has more choices, not just setting the state, but also can do the side effects with returning void. So as a event handler, useEvent provides more flexibility to handle the side effects.

comments powered by Disqus