State Management for a Functional React

Kory Tegman,

@koryteg

State Management Is hard

  • Hard to track down bugs.
  • Unintended side effects.
  • Feel like this:

thankfully we have React Hooks

I'm Kory.

  • Senior Software Engineer @ EchoBind
  • Full Stack Web Dev - 10 years
  • Lots of JavaScript
  • Lots of Ruby

The Progress of a React App.

  • start small.
  • add bunch of setState methods
  • throw props all over the place.
  • and oh no, we need a better solution.

 

Piles of Redux boilerplate

but now we have Hooks!

What are Hooks?

React.Component

  • setState
  • componentDidMount
  • componentWillUpdate
  • componentDidUpdate
  • componentWillUnmount

React Functional component

  • useState
  • useEffect
  • useContext
  • useReducer
  • useCallback
  • useMemo

Button that Increments

const { useState } = React;

const App = () => {
  const [count, setCount] = useState(0)
  return <div>
    <button onClick={() => setCount(count+1)} >increment</button>
    <br />
    count: {count}
  </div>
}

ReactDOM.render(<App />, document.getElementById('app'))

Hooks Examples

But really, an entire app with hooks?

how do we manage an application's worth of state?

Cover 3 Things

Have a single source of truth.

Memoize what you can

Minimize Your State

Single Source of Truth

import React, { Component } from "react";

class Main extends Component {
  state = {
    text: "HELLO!"
  };
  render() {
    return <First text={this.state.text} />;
  }
}
const First = props => <Second text={props.text} />
const Second = props => <Third text={props.text} />
const Third = props => <h3>Text is: {props.text}</h3>

export default Main;

Prop Drilling

import React from "react";
import { getUser } from "api/users";

const AppContext = React.createContext(null);

const App = () => {
  const user = getUser();
  return (
    <AppContext.Provider value={user}>
      <UserProfilePage />
    </AppContext.Provider>
  );
};

const UserProfilePage = () => {
  return (
    <AppContext.Consumer>
      {user => <UserHeader user={user} />}
    </AppContext.Consumer>
  );
};

Original Context API


const AppContext = React.createContext(null);

const App = () => {
  const user = getUser();
  return (
    <AppContext.Provider value={user}>
      <UserProfilePage />
    </AppContext.Provider>
  );
};

const UserProfilePage = () => {
  const user = React.useContext(AppContext);
  return (
    <React.Fragment>
      <UserHeader user={user} />
      {/* Render rest of your user page */}
    </React.Fragment>
  );
};

useContext

What are Reducers?

const AppContext = React.createContext(null);

const currentUserReducer = (state, action) => {
  switch (action.type) {
    case "UPDATE_AVATAR": {
      return { ...state, avatar: action.avatar };
    }
    default: {
      throw new Error(`passed invalid or unknown action.type: ${action.type}`);
    }
  }
};

const CurrentUserProvider = () => {
  const user = getUser();
  const [state, dispatch] = React.useReducer(currentUserReducer, user);
  return <AppContext.Provider value={[state, dispatch]} />;
};

useReducer

useReducer && useContext

const App = () => {
  return (
    <CurrentUserProvider>
      <UserProfilePage />
    </CurrentUserProvider>
  );
};

const UserProfilePage = () => {
  const [user, dispatch] = React.useContext(AppContext);
  return <React.Fragment>
    <UserProfileInfo userName={user.name} userEmail={user.email} />
    <UserProfileBody user={user} />
  </React.Fragment>
};

Reducer Actions

  • implement update avatar
  • make it async.
  • wrap it all up with our context

 

Reducer Action

const useCurrentUser = () => {
  const [state, dispatch] = React.useContext(AppContext);

  const updateAvatar = async avatar => {
    let user = await updateUser({ userId: state.id, avatar });
    dispatch({ type: "UPDATE_AVATAR", avatar: user.avatar });
  };
  return {state, dispatch, updateAvatar}
}

const UserProfilePage = () => {
  const {state, updateAvatar} = useCurrentUser();
  return <React.Fragment>
      <UserProfileInfo userName={state.name} userEmail={state.email} />
      <UserProfileAvatar userAvatar={state.avatar} updateAvatar={updateAvatar} />
      {/* render the rest of the profile page */}
  </React.Fragment>
};

Memoization

it’s a technique that executes a (pure) function once, saves the result in memory, and if we try to execute that function again with the same arguments as before, it just returns that previously saved result without executing the function again.

- Sam Pakvis

if anything on our state changes, everything re-renders.

Currently

Thankfully

React gives us a way to not re-render what has not changed

React Tools for memoizing

  • React.Memo
  • React.useMemo
  • React.useCallback

Teams App

const teamsReducer = (state, action) => {
  switch (action.type) {
    case "TOGGLE_TEAM":
      return { ...state, teams: /% update team %/ }
    default:
      throw new Error(`passed invalid or unknown action.type: ${action.type}`);
  }
};

Teams Reducer

const TeamsContext = createContext(null);

const useTeams = () => {
  const [state, dispatch] = useContext(TeamsContext);

  const toggleTeam = teamName => {
    dispatch({ type: "TOGGLE_TEAM", teamName });
  };

  return {
    state,
    dispatch,
    toggleTeam,
    pickedTeams: state.teams.filter(t => t.picked),
    unpickedTeams: state.teams.filter(t => !t.picked)
  };
};

Context and useTeams

const TeamsContextProvider = props => {
  const [state, dispatch] = useReducer(teamsReducer, {
    teams: Teams
  });
  return <TeamsContext.Provider value={[state, dispatch]} {...props} />;
};

const App = () => {
  return (
    <TeamsContextProvider>
      <TeamsPage />
    </TeamsContextProvider>
  );
};

Provider and App

const TeamsPage = () => {
  const { pickedTeams, unpickedTeams, toggleTeam } = useTeams();
  return (
    <ContainerComponent>
      <div className="bg-blue-200 w-1/2 m-5 rounded p-4">
        {unpickedTeams.map(team => (
          <Team key={team.name} action={toggleTeam} team={team} />
        ))}
      </div>
      <div className="bg-green-200 w-1/2 m-5 rounded p-4">
        {pickedTeams.map(team => (
          <Team key={team.name} action={toggleTeam} team={team} />
        ))}
      </div>
    </ContainerComponent>
  );
};

TeamsPage

const Team = ({ team, action }) => {
  // logs renders
  const renders = useRenders();
  return (
    <div onClick={() => action(team.name)} >
      {team.city} {team.name} - renders: {renders}
    </div>
  );
};

Team

React.Memo

const Team = React.memo(({ team, action }) => {
  // logs renders
  const renders = useRenders();
  return (
    <div onClick={() => action(team.name)} >
      {team.city} {team.name} - renders: {renders}
    </div>
  );
});

React.useCallback

const useTeams = () => {
  const [state, dispatch] = useContext(TeamsContext);

  const toggleTeam = useCallback(
    teamName => {
      dispatch({ type: "TOGGLE_TEAM", teamName });
    },
    [dispatch]
  );
///.... rest of useTeams////

No Re-Renders

React.useMemo

Minimize State

  • keeping data around too long gets stinky
  • Closest to the leaf nodes as possible
  • Let your router help

In a Single Page App

In a Single Page App

In a full stack framework

  • Next.js
  • Rails/django.

Common Mistakes

  • Deeply Nested Reducers
  • memoizing where it is not needed
  • passing your dispatcher around.
  • lacking clear boundaries.

Can I use this all today? 

  • React 16.8 - hooks landed
  • Just replace a component at a time.

What did we learn?

  1. Have a single Source of truth
  2. Memoize state
  3. Minimize state

Shout Outs

Thanks.

  • Get some stickers.
  • follow me on twitter: @koryteg
  • Echobind creates great software
  • go hawks.

 

State Management In React

By Kory Tegman

State Management In React

it is for a state management in react

  • 680