The State of React State in 2019
Becca Bailey • This Dot React Meetup
@beccaliz
Hi, I'm Becca!
@beccaliz
Chicago, IL
Why this talk?
@beccaliz
👩💻
@beccaliz
👩💻
❔
❔
❔
@beccaliz
@beccaliz
-
Clarify the problem
-
Explore some solutions
-
Make decisions
@beccaliz
State
@beccaliz
setState
useState
@beccaliz
Local State
@beccaliz
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
);
};
@beccaliz
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>
);
};
@beccaliz
Repetition
@beccaliz
Repetition is not your enemy.
@beccaliz
But sometimes it is.
Yay!
You win!
Oh no!
⚠️ Error!
@beccaliz
showModal
modalIsVisible
modalVisible
modalOpen
modalIsOpen
@beccaliz
😩 😡 😤
@beccaliz
Yay!
You win!
Oh no!
⚠️ Error!
@beccaliz
}
local state
@beccaliz
Global state
@beccaliz
}
global state
@beccaliz
}
semi-local state
@beccaliz
Flux Architecture
@beccaliz
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);
@beccaliz
😻
🤖
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;
}
};
@beccaliz
store
view
dispatch
props
parent
view
props
connected
presentational
@beccaliz
👍 Separation of Concerns 👍
@beccaliz
👎 Too much abstraction 👎
@beccaliz
// 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("😻");
});
@beccaliz
@beccaliz
@beccaliz
👍 Local State 👍
@beccaliz
👎 Prop Drilling 👎
👎 Duplication 👎
@beccaliz
✨ 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)
)
)
);
@beccaliz
✨ Context ✨
@beccaliz
Context is for dependency injection
@beccaliz
provider
view
state
consumer
helpers
@beccaliz
context provider
@beccaliz
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>
);
}
}
@beccaliz
✨ 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?
@beccaliz
Complexity
@beccaliz
👩💻
@beccaliz
How do we choose?
@beccaliz
You don't have to choose just one.
@beccaliz
What is the scope of the state?
@beccaliz
Is there a commonly-used library that can help?
@beccaliz
Am I repeating myself?
@beccaliz
Incremental Changes
@beccaliz
👩🏼💻
👨🏽💻
👩🏽💼
👨🏻💼
💭
💭
@beccaliz
👩🏼💻
👨🏽💻
👩🏽💼
👨🏻💼
💭
💭
👍
💯
💭
🎉
💭
🤩
✨ Thank you!! ✨
@beccaliz
Slides: https://noti.st/beccabailey
becca.is
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,183