Graduate Program KB

React hooks

History

  • React.createClass
    • Pass with an object with information to describe the component
    • JS didn't have a built in class system
  • React.Component
    • React v0.13.0
    • Allowed creating components from (now) native JS classes
    • Initialize the state of the component inside the constructor
    • If you're extending a subclass need to invoke super before using this
      • Also have to remember to pass props
      • Also have to .bind methods
    • createClass automatically bound all the methods to the component
  • Class Fields
    • Add instance properties directly on a class without using constructor
    • Can use arrow functions (note: instantiated for every object)
  • Duplicate logic
    • Have to sprinkle related logic throughout the component
    • Need 3 separate methods to keep state in sync
      • componentDidMount - Initial fetch
      • componentDidUpdate - Re fetch on state change
      • update method - Trigger the state change
  • Sharing non-visual logic
    • Have another component that needs the same kind of state
    • Could duplicate code - nah
    • Create a Higher order component with the logic and pass as props
    • Can get unwieldy
      • export default withHover(withTheme(withAuth(withRepos(Profile))))
      • Component tree now has a bunch of wrappers which makes it harder to follow

React hooks

  • Use a class if component has state, or needs lifecycle methods
  • Can use a function if just props and UI rendering
  • What if we could just use a function
  • State
    • useState
    • Is a hook
    • Takes a single argument - initial value for the state
    • Returns array with the first item being the sate and the second being a function to update the state
    const [loading, setLoading] = React.useState(true);
    
  • Lifecycle methods
    • Think about synchronization instead of lifecycle
    • React.useEffect
    • Allows performing side effects in function components
    • Arguments: function, optional array
      • function: side effects to run
      • array: when to re-run the effect
        • by default run after first render and every update
        • Instead array of state values will only trigger when that value changes (as well as mount, unmount)
          • Should include all values from the component scope that change and are used by the effect
          • Otherwise it will reference stale values from previous renders
        • Empty array will on mount and unmount
  • Sharing non-visual logic
    • Create your own hook
    • Function
      • Passed state objects
      • Synchronizes with useEffect
      • Returns state objects
    function useRepos(id) {
    const [repos, setRepos] = React.useState([]);
    const [loading, setLoading] = React.useState(true);
    
    React.useEffect(() => {
        setLoading(true);
    
        fetchRepos(id).then((repos) => {
        setRepos(repos);
        setLoading(false);
        });
    }, [id]);
    
    return [loading, repos];
    }
    
    function ReposGrid ({ id }) {
        const [ loading, repos ] = useRepos(id)
    }
    

Managing state

  • useState
  • Allows triggering a component re-render
  • Preserve values between renders
    • Unless you are using closures expect all variables in function to be GCd
    • React preserves state values between calls to the function to render
  • Each unique piece of state has its own useState invocation (and value, update function)
  • setState would merge new object with previous state (to just set a couple of values)
  • useState replaces, values should be separate
  • If the most logical data type for a piece of state is an object better to use useReducer
  • When updating state based on previous pass a function to the updater function, similar to setState
  • Lazy initialization
    function Counter() {
        const [count, setCount] = React.useState(getExpensiveCount());
        ...
    }
    
    • getExpensiveCount called for every Counter invocation, even though the result is only used once for the initial value
    • pass useState a function it will run the first time on the initial render and ignore later
    function Counter() {
        const [count, setCount] = React.useState(() => getExpensiveCount());
        ...
    }
    

Adding side effects

  • useEffect
  • side effect: State change that can be observed outside it's local environment
  • Add an effect
    React.useEffect(() => {
    document.title = "The new title.";
    });
    
  • Invoked after every render by default
    • After React has updated DOM - side effect won't block the UI update
  • Skipping side effects
    • Second argument
    • Array of state values
    • Will invoke whenever one of those values changes, instead of every render
  • Cleaning up side effect
    • If you return a function from the effect it will be invoked before the component is unmounted
    • If component is re-rendered the cleanup function will be invoked before the new render
      import { subscribe, unsubscribe } from './api'
      function Profile ({ username }) {
          const [profile, setProfile] = React.useState(null)
      
          React.useEffect(() => {
              subscribe(username, setProfile)
      
              return () => {
              unsubscribe(username)
              setProfile(null)
              }
          }, [username])
      
          if (profile === null) {
              return <p>Loading...</p>
          }
      
          return (
              <React.Fragment>
              <h1>@{profile.login}</h1>
              <img
                  src={profile.avatar_url}
                  alt={`Avatar for ${profile.login}`}
              />
              <p>{profile.bio}</p>
              </React.Fragment>
          );
      }
      
    • Cleanup will be invoked in two cases
      1. When username changes
      2. Before Profile is unmounted
    • NOTE: Cleanup is also done after render, but before re-running effect

Rules of hooks

  • Can only be called from the top-level of a function component or a custom hook
  • Not from function, class component, loop, if statement,event handler
  • React relies on the call order of Hooks to keep track of internal state and references

Creating custom hooks

  • Both higher order components and render props force you to restructure the tree and lead to heavy nesting
  • Decouple the non visual logic from the UI with a hook
  • create a function whose name start with use
    function useHover () {
        const [hovering, setHovering] = React.useState(false)
    
        const mouseOver = () => setHovering(true)
        const mouseOut = () => setHovering(false)
    
        const attrs = {
            onMouseOver: mouseOver,
            onMouseOut: mouseOut
        }
    
        return [hovering, attrs]
        }
    
    function Info ({ id, size }) {
        const [hovering, attrs] = useHover()
    
        return (
            <React.Fragment>
                {hovering === true
                ? <Tooltip id={id} />
                : null}
                <svg
                {...attrs}
                width={size}
                viewBox="0 0 16 16" >
                ...
                </svg>
            </React.Fragment>
        )
        }
    

Managing complex state

    let state = 0;
    nums.forEach((v) => state+=v);
  • forEach is dependent on state of application, also modifies state outside it's scope - impure
  • Best to avoid impure functions
  • Use reduce
  • Keeps track of state internally, accumulates into it with passed in function
const total = nums.reduce((state, value)=>(
    state+value
    ), 0);
  • useReducer
  • Adds state to a function component, but managed using the reducer pattern
  • Need a way for user actions to invoke our reducer function
  • Returns an array with the first element being the state and second being a dispatch which invokes the reducer
  • What you pass to dispatch will be passed as the second argument to the reducer
    function reducer(state, action) {
        if (action === "increment") {
            return state + 1;
        } else if (action === "decrement") {
            return state - 1;
        } else if (action === "reset") {
            return 0;
        } else {
            throw new Error(`This action type isn't supported.`);
        }
    }
    
    function Counter() {
        const [count, dispatch] = React.useReducer(reducer, 0);
    
        return (
            <React.Fragment>
            <h1>{count}</h1>
            <button onClick={() => dispatch("increment")}>+</button>
            <button onClick={() => dispatch("decrement")}>-</button>
            <button onClick={() => dispatch("reset")}>Reset</button>
            </React.Fragment>
        );
    } 
    
    • Decoupled update logic of count state from the component
    • Mapping actions to state transitions - separate how state updates from the action
    • Changing by a variable amount
    function reducer(state, action) {
        if (action.type === "increment") {
            return {
            count: state.count + state.step,
            step: state.step,
            };
        } else if (action.type === "decrement") {
            return {
            count: state.count - state.step,
            step: state.step,
            };
        } else if (action.type === "reset") {
            return {
            count: 0,
            step: state.step,
            };
        } else if (action.type === "updateStep") {
            return {
            count: state.count,
            step: action.step,
            };
        } else {
            throw new Error(`This action type isn't supported.`);
        }
    }
    
    function Counter() {
        const [count, dispatch] = React.useReducer(reducer, 0);
        return (
            <React.Fragment>
            <Slider
            onChange={(value) =>
                dispatch({
                type: "updateStep",
                step: value,
                })
            }
            />
            <hr />
            <h1>{state.count}</h1>
            <button
            onClick={() =>
                dispatch({
                type: "increment",
                })
            }
            >
            +
            </button>
            <button
            onClick={() =>
                dispatch({
                type: "decrement",
                })
            }
            >
            -
            </button>
            <button
            onClick={() =>
                dispatch({
                type: "reset",
                })
            }
            >
            Reset
            </button>
        </React.Fragment>
        );
    }
    
    • Reducer better for updating state that depends on other state
    • Declarative state updates
      • Registration form with username, email, password
      • UX state: loading, error, registered
        function Register() {
            const [username, setUsername] = React.useState("");
            const [email, setEmail] = React.useState("");
            const [password, setPassword] = React.useState("");
            const [loading, setLoading] = React.useState(false);
            const [error, setError] = React.useState("");
            const [registered, setRegistered] = React.useState(false);
        
            const handleSubmit = (e) => {
                e.preventDefault();
        
                setLoading(true);
                setError("");
        
                newUser({ username, email, password })
                .then(() => {
                    setLoading(false);
                    setError("");
                    setRegistered(true);
                })
                .catch((error) => {
                    setLoading(false);
                    setError(error);
                });
            };
        
            if (registered === true) {
                return <Redirect to="/dashboard" />;
            }
        
            if (loading === true) {
                return <Loading />;
            }
        
            return (
                <React.Fragment>
                {error && <p>{error}</p>}
                <form onSubmit={handleSubmit}>
                    <input
                    type="text"
                    placeholder="email"
                    onChange={(e) => setEmail(e.target.value)}
                    value={email}
                    />
                    <input
                    type="text"
                    placeholder="username"
                    onChange={(e) => setUsername(e.target.value)}
                    value={username}
                    />
                    <input
                    placeholder="password"
                    onChange={(e) => setPassword(e.target.value)}
                    value={password}
                    type="password"
                    />
                    <button type="submit">Submit</button>
                </form>
                </React.Fragment>
            );
        }
        
      • Declarative approach - describe what we want to accomplish instead of how
      • Reducer encapsulates imperative, instructional code
        function registerReducer(state, action) {
            if (action.type === "login") {
                return {
                ...state,
                loading: true,
                error: "",
                };
            } else if (action.type === "success") {
                return {
                ...state,
                loading: false,
                error: "",
                registered: true,
                };
            } else if (action.type === "error") {
                return {
                ...state,
                loading: false,
                error: action.error,
                };
            } else if (action.type === "input") {
                return {
                ...state,
                [action.name]: action.value,
                };
            } else {
                throw new Error(`This action type isn't supported.`);
            }
        }
        
        const initialState = {
            username: "",
            email: "",
            password: "",
            loading: false,
            error: "",
            registered: false,
        };
        
        function Register() {
            const [state, dispatch] = React.useReducer(
                registerReducer,
                initialState,
            );
        
            const handleSubmit = (e) => {
                e.preventDefault();
        
                dispatch({ type: "login" });
        
                newUser({
                username: state.username,
                email: state.email,
                password: state.password,
                })
                .then(() => dispatch({ type: "success" }))
                .catch((error) =>
                    dispatch({
                    type: "error",
                    error,
                }),
            );
        };
        
        if (state.registered === true) {
            return <Redirect to="/dashboard" />;
        }
        
        if (state.loading === true) {
            return <Loading />;
        }
        
        return (
            <React.Fragment>
            {state.error && <p>{state.error}</p>}
            <form onSubmit={handleSubmit}>
                <input
                type="text"
                placeholder="email"
                onChange={(e) =>
                    dispatch({
                    type: "input",
                    name: "email",
                    value: e.target.value,
                    })
                }
                value={state.email}
                />
                <input
                type="text"
                placeholder="username"
                onChange={(e) =>
                    dispatch({
                    type: "input",
                    name: "username",
                    value: e.target.value,
                    })
                }
                value={state.username}
                />
                <input
                placeholder="password"
                onChange={(e) =>
                    dispatch({
                    type: "input",
                    name: "password",
                    value: e.target.value,
                    })
                }
                value={state.password}
                type="password"
                />
                <button type="submit">Submit</button>
            </form>
            </React.Fragment>
        );
        
    • Minimizing useEffect dependency array
      • Leave it off - infinite loop of it always updating
      • Forget to add values - stale data
    • State values are encapsulated inside reducer, don't need to add to array
          React.useEffect(() => {
              const id = window.setInterval(() => {
                  setCount(count + 1);
              }, 1000);
      
              return () => window.clearInterval(id);
          }, [count]);
      
      • Every time count changes a new interval is created
      React.useEffect(() => {
          const id = window.setInterval(() => {
              dispatch({ type: "increment" });
          }, 1000);
      
          return () => window.clearInterval(id);
      }, []);
      
      • Can do this example without reducer
        React.useEffect(() => {
        const id = window.setInterval(() => {
            setCount((count) => count + 1)
        }, 1000)
        
        return () => window.clearInterval(id)
        }, [])
        
      • Count doesn't need to be in the array because the value comes from setCount instead of the function component
      • But if we add a step variable to add instead of 1 adds a dependency to the array
        • setCount passes the old variable of count to be updated
        • Need to get the up to date value of step - rerun effect when value changes
        • Putting step, count in a reducer means it would be provided by the setState function

New render, same value

  • useRef hook
  • Not dealing with ui - don't need to re-render
  • But want to persist value
  • Don't want to call set state since it will re-render
    function usePersistentValue(initialValue) {
        return React.useState({
            current: initialValue,
        })[0];
    }
    
  • useRef does this
  • Gives object with current property that persists across renders
        const nameRef = React.useRef();
    
        <input placeholder="name" type="text" ref={nameRef} />
    
  • When passing a ref to the ref prop on a DOM element react will set the ref's current property to the corresponding DOM element
  • Persisting a handle across renders
    function Counter() {
        const [count, setCount] = React.useState(0);
    
        let id = React.useRef(null);
    
        const clear = () => {
            window.clearInterval(id.current);
        };
    
        React.useEffect(() => {
            id.current = window.setInterval(() => {
            setCount((c) => c + 1);
            }, 1000);
    
            return clear;
        }, []);
    
        return (
            <div>
            <h1>{count}</h1>
            <button onClick={clear}>Stop</button>
            </div>
        );
    }
    

React contexts

  • See (react)[./react]
  • useMemo
    • Makes sure the value it returns stays the same unless variable from the list changes
      export default function App () {
        const [locale, setLocale] = React.useState('en')
      
        const toggleLocale = () => {
          setLocale((locale) => {
            return locale === 'en' ? 'es' : 'en'
          })
        }
      
        const value = React.useMemo(() => ({
          locale,
          toggleLocale
        }), [locale])
      
        return (
          <LocaleContext.Provider value={value}>
            <Home />
          </LocaleContext.Provider>
        )
      }
      
    • Constructing a new function each invocation, but it doesn't trigger a re-render because useMemo is hiding the change
  • useContext
    • Render-props syntax gets messy, especially with nested consumers
      export default function Nav() {
        return (
          <AuthedContext.Consumer>
            {({ authed }) =>
              authed === false ? (
                <Redirect to="/login" />
              ) : (
                <LocaleContext.Consumer>
                  {({ locale, toggleLocale }) =>
                    locale === "en" ? (
                      <EnglishNav toggleLocale={toggleLocale} />
                    ) : (
                      <SpanishNav toggleLocale={toggleLocale} />
                    )
                  }
                </LocaleContext.Consumer>
              )
            }
          </AuthedContext.Consumer>
        );
      }
      
    • useContext takes a context object and returns what was passed to the value prop of the nearest provider
    • More composable API
      export default function Nav() {
        const { authed } = React.useContext(AuthedContext);
      
        const { locale, toggleLocale } = React.useContext(LocaleContext);
      
        if (authed === false) {
          return <Redirect to="/login" />;
        }
      
        return locale === "en" ? (
          <EnglishNav toggleLocale={toggleLocale} />
        ) : (
          <SpanishNav toggleLocale={toggleLocale} />
        );
      }
      

Performance

  • Same use intuition as functions for components
  • Instead of function taking arguments and returning value, component takes props and returns ui (jsx)
  • Hooks allow components to just be functions
  • Like a function any code int he component will be executed when it re-renders
  • React.memo HOC that lets you skip re-rendering a component if it's props haven't changed
  • When components is wrapped in React.memo() react will render the component and memoize the result
  • On re-render React will do a shallow comparison of the old and new props, if they match will skip re-render
  • Primitive vs reference values
    • React.memo uses the identity function to compare props (==)
    • When passing in a new object or function it will have a new reference and not match the previous value
    • Can pass a second argument to React.memo with a custom comparison, or use useCallback
  • useCallback
    • Returns a memoised callback
    • Function won't be re-created every render
    • Second param is a dependency list which will trigger re-creating the function
    • NOTE: NthFib and NthPrime have been exported wrapped in React.memo
      function App() {
        const [fibCount, setFibCount] = React.useState(1)
        const [primeCount, setPrimeCount] = React.useState(1)
      
        const handleReset = () => {
          setFibCount(1)
          setPrimeCount(1)
        }
      
        const add10 = () => {
          setFibCount((c) => c + 10)
          setPrimeCount((c) => c + 10)
        }
      
        const incrementFib = React.useCallback(() =>
          setFibCount((c) => c + 1),
          []
        )
      
        const incrementPrime = React.useCallback(() =>
          setPrimeCount((c) => c + 1),
          []
        )
      
        return (
          <React.Fragment>
            <button onClick={add10}>Add 10</button>
            <button onClick={handleReset}>Reset</button>
            <hr />
            <NthFib
              count={fibCount}
              increment={incrementFib}
            />
            <hr />
            <NthPrime
              count={primeCount}
              increment={incrementPrime}
            />
          </React.Fragment>
        );
      }
      
  • useMemo
    • Could useMemo the result of the computation instead of the whole sub-component
    • Don't use useMemo to persist values
      • React treats it as a performance hint instead of a guarantee, it might forget the memoised value in certain situations