The State of React State in 2019

Becca Bailey  •  This Dot React Meetup

Hi, I'm Becca!

Why this talk?

👩‍💻

👩‍💻

  1. Clarify the problem

  2. Explore some solutions

  3. Make decisions

State

setState
useState

Local State

Yay!

You win!

class MyComponent extends React.Component {
  state = {
    visible: false
  };
  
  showModal() {
    this.setState(state => ({
      visible: true
    }));
  }

  hideModal() {
    this.setState(state => ({
      visible: false
    }));
  }
  
  render() {
    ...stuff here
  }
}
  
const MyComponent = () => {
  const [visible, setVisible] = React.useState(false);
  
  function showModal() {
    setVisible(true);
  }
  
  function hideModal() {
    setVisible(false);
  }
  
  return (
    ...stuff here
  );
};

Prop Drilling

// App.js
const App = () => {
  
  return (
    <Container>
      <Game></Game>
    </Container>
  );
};
const user = {
  id: 123,
  firstName: "Becca",
  lastName: "Bailey",
  email: "beccanelsonbailey@gmail.com",
  marker: "👩‍💻"
}
// App.js
const App = () => {
  const [user, updateUser] = React.useState();
  
  React.useEffect(async () => {
    const user = await fetchLoggedInUser();
    updateUser(user);
  }, [])
  
  return (
    <Container>
      <Game user={user}></Game>
    </Container>
  );
};
// Game.js
const Game = ({ user }) => {
  const [board, updateBoard] = React.useState(EMPTY_BOARD);

  function makeMove(index) {
    updateBoard({...board, [index]: user.marker })
  }

  return (
    <React.Fragment>
      <h1>Hello {user.name}!</h1>
      <Board board={board} makeMove={makeMove} />
    </React.Fragment>
  );
};
// Game.js
const Game = ({ user }) => {
  const [board, updateBoard] = React.useState(EMPTY_BOARD);

  function makeMove(index) {
    updateBoard({...board, [index]: user.marker })
  }

  return (
    <React.Fragment>
      <Greeting user={user} />
      <Board board={board} makeMove={makeMove} />
    </React.Fragment>
  );
};

Repetition

Repetition is not your enemy.

But sometimes it is.

Yay!

You win!

Oh no!

⚠️ Error!

showModal
modalIsVisible
modalVisible
modalOpen
modalIsOpen

😩 😡 😤

Yay!

You win!

Oh no!

⚠️ Error!

}
local state

Global state

}
global state
}
semi-local state

Flux Architecture

action
reducer
store
view
// actions.js
export function makeMove(index) {
  return {
    type: "MAKE_MOVE",
    payload: { index }
  };
}

// reducers.js
const game = (state = getInitialState(), action) => {
  switch (action.type) {
    case "MAKE_MOVE": {
      const { index } = action.payload;
      return {
        ...state,
        board: {
          ...state.board,
          [index]: state.currentPlayer.marker,
        }
      };
    }

    default:
      return state;
  }
};
// Game.js
function mapStateToProps({ board }) {
  return board;
}

function mapDispatchToProps(dispatch) {
  return {
    makeMove: (index) => {
      dispatch(makeMove(index));
    }
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Game);

😻

🤖

Becca played at spot 0

Computer played at spot 4

😻

Becca played at spot 7

🤖

Computer played at spot 3

// actions.js
export function makeMove(index) {
  return {
    type: "MAKE_MOVE",
    payload: { index }
  };
}

// reducers.js
const game = (state = getInitialState(), action) => {
  switch (action.type) {
    case "MAKE_MOVE": {
      const { index } = action.payload;
      return {
        ...state,
        board: {
          ...state.board,
          [index]: state.currentPlayer.marker,
        }
      };
    }

    default:
      return state;
  }
};
store
view
dispatch
props
parent
view
props

connected

presentational

👍 Separation of Concerns 👍

👎 Too much abstraction 👎

// Board.test.tsx

it("handles click events", () => {
  const props = {
    makeMove: jest.fn(),
    board: DEFAULT_BOARD
  };
    
  const { queryAllByRole } = render(<Board {...props}></Board>);

  fireEvent.click(queryAllByRole("button")[0]);

  expect(props.makeMove).toHaveBeenCalledWith(0);
});
  
// reducers.test.js

it("updates the board state", () => {
  expect(
    reducer(initialState, {
      type: "MAKE_MOVE",
      payload: { index: 2 }
    })
  ).toEqual({
    ...initialState,
    board: createBoard(`
      - - X
      - - - 
      - - - 
    `),
  });
});
it("allows a player to make a move", () => {
  const { getByTitle } = render(<Game />);
  const spot = getByTitle("0");

  fireEvent.click(spot);

  expect(getByTitle("0").textContent).toEqual("😻");
});
it("allows a player to make a move", () => {
  const { getByTitle } = render(<Game />);
  const spot = getByTitle("0");

  fireEvent.click(spot);

  expect(getByTitle("0").textContent).toEqual("😻");
});

👍 Local State 👍

👎 Prop Drilling 👎

👎 Duplication 👎

✨ Higher Order Components ✨

✨ Render Props ✨

<ModalManager>
  {({ showModal, hideModal, visible }) => (
    <React.Fragment>
      <Button onClick={() => showModal()}>Click me!</Button>
      <Modal visible={visible}>
        <h1>You win!</h1>
        <Button onClick={() => hideModal()}>Close</Button>
      </Modal>
    </React.Fragment>
  )}
</ModalManager>
const ModalManager = ({ children }) => {
  const [visible, setVisible] = React.useState(false);

  function showModal() {
    setVisible(true)
  };

  function hideModal {
    setVisible(false)
  };

  render() {
    return (
      <React.Fragment>
        {children({ visible, showModal, hideModal })}
      </React.Fragment>
    )
  }
}
<Query query={LOGGED_IN_USER}>
  {({ loading, error, data }) => {
    if (loading) {
      return <Spinner />;
    }
    if (error) {
      return <Error message={error} />;
    }
    return <Profile user={data}/>;
  }}
</Query>
<Container>
  <Query query={LOGGED_IN_USER}>
    {({ loading, error, data }) => {
      if (data) {
        return (
          <ModalManager>
            {({ showModal, hideModal, visible }) => {
              return (
                <React.Fragment>
                  <Button onClick={() => showModal()}>Click me!</Button>
                  {visible && (
                    <Modal>
                      <h1>Hello {data.user.name}!</h1>
                      <Button onClick={() => hideModal()}>Close</Button>
              	    </Modal>
                  )}
                </React.Fragment>
              );
            }}
          </ModalManager> 
        )
      }
    }}
  </Query>
</Container>
// Game.js
function mapStateToProps({ board }) {
  return board;
}

function mapDispatchToProps(dispatch) {
  return {
    makeMove: (index) => {
      dispatch(makeMove(index));
    }
  };
}

export default connect(
  mapStateToProps,
  mapDispatchToProps
)(Game);
export default withRouter(
  withTheme(
    withSomeOtherState(
      connect(
        mapStateToProps,
        mapDispatchToProps
      )(Game)
    )
  )
);

✨ Context ✨

Context is for dependency injection

provider
view

state

consumer

helpers

context provider
context provider
// GreetingModal.js
function GreetingModal() {
  const { user } = React.useContext(LoggedInUserContext);
  const { hideModal } = React.useContext(ModalContext);
  
  return (
    <Modal id="greeting">
      <h1>Hello {user.name}!</h1>
      <Button onClick={() => hideModal()}>Close</Button>
    </Modal>
  )
}
const ModalProvider = ({ children }) => {
  const [visible, setVisible] = React.useState(false);

  function showModal() {
    setVisible(true)
  };

  function hideModal {
    setVisible(false)
  };

  render() {
    return (
      <ModalContext.Provider values={{ visible, showModal, hideModal }}>
        {children}
      </ModalContext.Provider>
    );
  }
}

✨ Hooks ✨

// reducers/game.js
function useGame(initialState) {
  const [game, dispatch] = React.useReducer(gameReducer);

  function makeMove(index) {
    return dispatch({ type: "MAKE_MOVE", payload: index });
  }

  return { game, makeMove };
}
function gameReducer(state, action) {
  switch (action.type) {
    case "MAKE_MOVE": {
      const index = action.payload;
      const { currentPlayer, players } = state;
      const nextPlayer = switchPlayer(currentPlayer, players);
      return {
        ...state,
        board: {
          [index]: currentPlayer.marker,
          currentPlayer: nextPlayer,
          //...etc
        }
      };
    }
    default: {
      return state;
    }
  }
}
it("allows a player to make a move", () => {
  const { getByTitle } = render(<Game />);
  const spot = getByTitle("0");

  fireEvent.click(spot);

  expect(getByTitle("0").textContent).toEqual("😻");
});

Do you need Redux?

Complexity

👩‍💻

How do we choose?

You don't have to choose just one.

What is the scope of the state?

Is there a commonly-used library that can help?

Am I repeating myself?

Incremental Changes

👩🏼‍💻

👨🏽‍💻

👩🏽‍💼

👨🏻‍💼

💭

💭

👩🏼‍💻

👨🏽‍💻

👩🏽‍💼

👨🏻‍💼

💭

💭

👍

💯

💭

🎉

💭

🤩

✨ Thank you!! ✨

Copy of The State of React State in 2019

By Becca Nelson

Copy of The State of React State in 2019

Talk for This Dot React Online

  • 1,011