What I know about React so far Part 2

In notebook:
Work Notes
Created at:
2019-07-01
Updated:
2019-08-15
Tags:
React libraries JavaScript Performance

More on useEffect

The second argument controls when and how often it's called

    const [time, setTime] = useState(new Date());

  useEffect(() => {
    // notice that we are not using
    // setInterval 
    // because we are always rerendering
    // it could be done with setInterval as well though
    const timer = setTimeout(setTime(new Date()), 1000);
    return () => clearTimeout(timer);
    // it keeps running, because we didn't provide a
    // list of dependencies, as the second parameter
  });

If we had done useEffect(() => {} []) with the [] as the second parameter, it would have only ran once.

According to the React team, you should do }, [setTime]);, because it's a dependency, even though it's also a function and so would never change. On the other hand, if you would do }, [time]); it would again run every 1000ms, because time does change

useEffect cleanup

Is done by the function it returns, so in the above example it's the return () => clearTimeout(timer); bit.

It's a great pattern when you need to integrate external libraries in your app.

Sharing state in Reach

The simplest solution is to move the state and event handlers up from the children and pass them down as props. And also pass the event handlers down as props:

  <MyComponent path="/results" handleFooChange={this.handleFooChange} />

// and later, on the child:

<button onClick={this.props.handleFooChange} />

In most situation this is enough, but for more complex scenarios Context is the advanced solution.

Note on spreading object properties

Brian Holt doesn't recommend it, as it's obscures what exactly you are passing down, and what exactly (which subset) your component needs. He doesn't recommend using <MyComponent> {...this.props} /> either based on his experience of working with React.

In fact when you do <MyComponent {...obj} />, you are using the JSX version of the spread operator.

Context

Context is a state alternative, that you can update anywhere in your React tree. It can replace the need for Redux. Available since React 16.

The idea is that you have a Context Provider and Consumer. Often you would create a React.createContext and pass it an "empty" object, where the properties do exist, but their values are empty. It's mostly for testing purposes and TypeScript.

Then in your App.js (root compoonent), you initialise the this.state, then, you wrap your child components in <Provider value={this.state}><MyComponent /></Provider>.

  // MyContext.js 
const MyContext = React.createContext({
  name: '',
  handleNameChange: () {}
})

// then later in
// App.js
import { Provider } from './MyContext'

class App extends React.Component {
  constructor (props) {
    super(props)

    this.state = {
      name: 'foo',
      this.handleNameChange: handleNameChange
    }
  }
  handleNameChange = event => {
    this.setState({
      name: event.target.value
    })
  }
  render () {
    return (
      <div>
        <Provider value={this.state}>
          <MyComponent />
        </Provider>
      </div>
    )
  }
}

// and then in
// MyComponent
import { Consumer } from './MyComponent'

class MyComponent extends React.Component {
  render() {
    return (
      <Consumer>
        {function(context){
        // and here `context` is the same
        // as the state defined in App.js
          return (
            // your markup here
            // and change
            // this.state.whatever
            // to
            // context.whatever
          )
        }}
      </Consumer>
    )
  }

}

Of course, MyComponent can be anywhere in the React tree, as deep as you want and this has access to this context.

Accessing Context in lifecycle methods

Since <Context> is a component and so is always in render, you need to wrap your whole component with it, best done at the export declaration:

  export default function MyComponentWithContext(props) {
  return (
    <Consumer>
      {context => <MyComponent {...props} mycontext={context} />}
    </Consumer>
  )
}

// and now you can access all Context values as props
// this.props.mycontext.name

From the V4 of the course:

Creating a context, with an indentity function for the updater:

  import React, { useState, useContext, createContext } from "react";

// this is just a "fake" object
// just to let people know the shape of the object
const UserContext = createContext([
  {
    firstName: "Bob",
    lastName: "Bobberson",
    suffix: 1,
    email: "bobbobberson@example.com"
  },
  // the updater function o
  // "identity" function 
  // so it's the users of the context
  // who will update the state (think immutable!)
  // continue reading at `ContextComponent`
  obj => obj
]);

// ... later you wrap your app with the context
// this is the top level component 
const ContextComponent = () => {
// **** 1. create the hook.  ↴
  const userState = useState({
    firstName: "James",
    lastName: "Jameson",
    suffix: 1,
    email: "jamesjameson@example.com"
  });

  return (
   // **** 2. wrapping it in the context.  ↴
    <UserContext.Provider value={userState}>
      <h1>first level</h1>
      <LevelTwo />
    </UserContext.Provider>
  );
};

then later, you would update it with immutable operations, because the updater function is just an identity function:

  // ...
const [user, setUser] = useContext(UserContext);

// then later...
<button
 // **** 5. the increment function works here too.  ↴
 // using the hook, and context
 // which is on a grand-grand-grand parent...
  onClick={() => {
    setUser(Object.assign({}, user, { suffix: user.suffix + 1 }));
  }}
>
  Increment
</button>

useRef vs useState values

The useRef hook was introduced in the Portals presentation. We used it to hold a reference to the "other" DOM root element that would host the modal component in the opened state.

  //   ****   Ref.js   ****

import React, { useState, useEffect, useRef } from "react";

const RefComponent = () => {
  const [stateNumber, setStateNumber] = useState(0);
  const numRef = useRef(0);

   // **** 1. here the stateNumber and numRef.  ↴
   // won't match
   // they are incremented at the same time,
   // but they are not the same
   // stateNumber "lags" behind
  function incrementAndDelayLogging() {
  // what's going on, is that this `stateNumber` below, is
  // is holding onto the previous iteration
  // because of closure
    setStateNumber(stateNumber + 1);
  // while numRef is always holding onto to the
  // CURRENT value
    numRef.current++;
    setTimeout(
      () => alert(`state: ${stateNumber} | ref: ${numRef.current}`),
      1000
    );
  }

  return (
    <div>
      <h1>useRef Example</h1>
      <button onClick={incrementAndDelayLogging}>delay logging</button>
      <h4>state: {stateNumber}</h4>
      <h4>ref: {numRef.current}</h4>
    </div>
  );
};

export default RefComponent;

So what happens is that calling setStateNumber does update the stateNumber, but in the function that called it (incrementAndDelayLogging), the stateNumber is still pointing to the value before the setStateNumber call. While the numRef.current is directly pointing to the latest value.

This presents a great case for useRef. In your render function, the stateNumber value will always be the correct one, but elsewhere, as the above example demonstrated, it's better to use useRef. It's useful to hold on to DOM elements, intervals, etc.

Notice, that you can only use .current here, no other property, the object is sealed.

Reducers (Redux replacement)

useReducer combined with Context can replace entirely the need for Redux.

  import React, { useReducer } from "react";

const reducer = (state, action) => {
  switch (action.type) {
    case 'increment':
      return Object.assign({}, state, {counter: state.counter++})
    case 'decrement':
      return Object.assign({}, state, {counter: state.counter--})
    default:
       return saate
  }
}

const ReducerComponent = () => {
  const [{counter}, dispatch] = useReducer(reducer, {counter})
}

return (
  <div>
    <button onClick={() => dispatch({ type: 'increment' })}>➕</button>
    <button onClick={() => dispatch({ type: 'decrement' })}>➖</button>
  </div>
)

Again, combining useReducer with Context is a great way to manage the global state of your application and replace Redux.

Performance optimizations

useMemo to optimise render time calculations

You can use useMemo to memoise expensive calculations that would happen inside your render function. Works similar to other memoisation function form other libraries.

  import React, { useState, useMemo } from "react";

const fibonacci = n => {
  if (n <= 1) {
    return 1;
  }

  return fibonacci(n - 1) + fibonacci(n - 2);
};

const MemoComponent = () => {
  const [num, setNum] = useState(1);
  // ...
  const fib = useMemo(() => fibonacci(num), [num]);
  return (
    <div>
      Fibonacci of {num} is {fib}
      <button onClick={() => setNum(num + 1)}>➕</button>
    </div>
  )
}

useCallback to partially render your component

You can combine useCallback and memo to memoise the rendering of a part of your component.

  import React, { useState, useEffect, useCallback, memo } from "react";

const ExpensiveComputationComponent = memo(({ compute, count }) => {
  return (
    <div>
      <h1>computed: {compute(count)}</h1>
      <h4>last re-render {new Date().toLocaleTimeString()}</h4>
    </div>
  );
});

// then later...
return (
  <div>
    <ExpensiveComputationComponent
     // **** 4. use the useCallback.  ↴
     // same as useMemo
     // give it a function and the list of dependencies
      compute={useCallback(fibonacci, [])}
      count={count}
    />
  </div>
)

don't overuse useCallback and useMemo!

Only add them, when you start to have a problem. If you add them too early, then you can quickly make a huge mess...

useLayoutEffect

It's useful when you need to measure things in the DOM. (getBoundClientRect, etc).

  import React, { useState, useLayoutEffect, useRef } from "react";

const LayoutEffectComponent = () => {
  const [width, setWidth] = useState(0);
  const [height, setHeight] = useState(0);
   // **** 2. storing the measurements here.  ↴
  const el = useRef();

   // **** 1. we're measuring the elements here.  ↴
   // **** 4. it actually kicks off another rerender.  ↴
   // it calls the LayoutEffectComponent twice,
   // but actually only paints once
  useLayoutEffect(() => {
    setWidth(el.current.clientWidth);
    setHeight(el.current.clientHeight);
  });
  // useLayoutEffect works just like any other Effect
  // you could do
  // }, [])
  // to add dependencies
  

  return (
    <div>
      <h1>useLayoutEffect Example</h1>
      <h2>textarea width: {width}px</h2>
      <h2>textarea height: {height}px</h2>
      <textarea
        onClick={() => {
         // **** 3. we're setting unintuitively to 0 .  ↴
         // has no meaning,
         // but it kicks off the rerender
          setWidth(0);
        }}
        // this will store the actual DOM element,
        // passing it to useLayoutEffect
        // console.log(el)
        // would show the actual textarea element
        ref={el}
      />
    </div>
  );
};

export default LayoutEffectComponent;

The difference between useEffect and useLayoutEffect

useEffect is (technically) asynchronous, it happens sometime after the rendering, while useLayoutEffect is more synchronous, as it's triggered right after the rendering has taken place.

(notice the word technically, the difference is still very small)

Redux

If you have a lot of asynchronous code in your app, for example making dozens of API calls. Even Dan Abramov the creator Redux, says that he doesn't use them much anymore.

The steps of a Redux circle,

original source

1 User types in input box 2 Call action creator to get an action 3 Dispatch action to Redux 4 Redux inserts the action into the root reducer 5 The root reducer delegates that action to the correct reducer 6 The reducer returns a new state given the old state and the action object 7 That new state becomes the store's state 8 React is then called by Redux and told to update

Redux is not React specific, you can use it with other frameworks such as Angular or Ember, or even without a framework.

Creating a Redux store

Import createStore and initialise it with a reducer.

  //    ****        store.js        ****

import { createStore } from "redux";
import reducer from "./reducers";

const store = createStore(
  reducer,
  // this is just for using 
  // React devtools
  // if React devtools exist, use them
  // otherwise just give a "bogus" function (`f => f`)
  typeof window === "object" &&
    typeof window.__REDUX_DEVTOOLS_EXTENSION__ !== "undefined"
    ? window.__REDUX_DEVTOOLS_EXTENSION__()
    : f => f
);

export default store;

Creating the reducers

This is just some more boilerplate.

   //    ****        reducers/index.js        ****

import { combineReducers } from "redux";
import location from "./location";

export default combineReducers({
  location: location
});

Then finally one reducer

   //    ****        reducers/location.js        ****

 // adding a default value for state...
export default function location(state = "Seattle, WA", action) {
  switch (action.type) {
    case "CHANGE_LOCATION":
      return action.payload;
    default:
      return state;
  }
}

the action object

The action object above is provided by redux.

You should always return a state this is why switch with a default case is great.

The payload property

While the action property is a hard requirement, payload is part of the flux standard action. It's available as a GitHub repo. It's a description of the action shapes.

Creating and dispatching actions

You can dispatch them directly, or use actionCreators that return the action definitions.

   //    ****        actionCreators/changeLocation.js        ****

export default function changeLocation(location) {
  return { type: "CHANGE_LOCATION", payload: location };
}

Connecting Redux to your app

First, you will no longer be using useState hooks, but using the store we created above.

  import { Provider } from "react-redux";
import store from './store'

const App = () => {
  <Provider store={store}>
    // .. app code
  </Provider>
}

Then in one of your component

  import { connect } from "react-redux";
// read the props...
const mapStateToProps = ({ theme }) => ({ theme });

// connect it ↴
const WrappedDetails = connect(mapStateToProps)(Details);

return (
  <WrappedDetails {...props} />
)