Skip to content

Instantly share code, notes, and snippets.

@pbojinov
Last active August 20, 2022 19:42
Show Gist options
  • Star 7 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save pbojinov/d13316100f66f4e3f29f80aa8ace0433 to your computer and use it in GitHub Desktop.
Save pbojinov/d13316100f66f4e3f29f80aa8ace0433 to your computer and use it in GitHub Desktop.
useReducer vs useState vs useContext – https://www.robinwieruch.de/react-usereducer-vs-usestate/

Summary

Use useState:

  1. if you manage JavaScript primitives as state
  2. if you have simple state transitions
  3. if you want to have business logic within your component
  4. if you have different properties that don’t change in any correlated manner and can be managed by multiple useState hooks
  5. if your state is co-located to your component
  6. if you’ve got a small application (but the lines are blurry here)

Use useReducer:

  1. if you manage JavaScript objects or arrays as state
  2. if you have complex state transitions
  3. if you want to move business logic into reducers
  4. if you have different properties that are tied together and should be managed in one state object
  5. if you want to update state deep down in your component tree
  6. if you’ve got a medium size application (but the lines are blurry here)
  7. if you want have an easier time testing it
  8. if you want a more predictable and maintainable state architecture

Use useContext

  1. if you need to share “global” State. It's a valid alternative to avoid the prop drilling (passing props through each component level)
/*
This example uses the previous two methods, but also incorporates useContext
to avoid prop drilling (passing props through each component level).
Source: https://www.robinwieruch.de/react-state-usereducer-usestate-usecontext/#usecontext-context-hook
*/
import React, {
useState,
useReducer,
useContext,
createContext,
} from 'react';
import uuid from 'uuid/v4';
const TodoContext = createContext(null);
const initalTodos = [
{
id: uuid(),
task: 'Learn React',
complete: true,
},
{
id: uuid(),
task: 'Learn Firebase',
complete: true,
},
{
id: uuid(),
task: 'Learn GraphQL',
complete: false,
},
];
const filterReducer = (state, action) => {
switch (action.type) {
case 'SHOW_ALL':
return 'ALL';
case 'SHOW_COMPLETE':
return 'COMPLETE';
case 'SHOW_INCOMPLETE':
return 'INCOMPLETE';
default:
throw new Error();
}
};
const todoReducer = (state, action) => {
switch (action.type) {
case 'DO_TODO':
return state.map(todo => {
if (todo.id === action.id) {
return { ...todo, complete: true };
} else {
return todo;
}
});
case 'UNDO_TODO':
return state.map(todo => {
if (todo.id === action.id) {
return { ...todo, complete: false };
} else {
return todo;
}
});
case 'ADD_TODO':
return state.concat({
task: action.task,
id: uuid(),
complete: false,
});
default:
throw new Error();
}
};
const App = () => {
const [filter, dispatchFilter] = useReducer(filterReducer, 'ALL');
const [todos, dispatchTodos] = useReducer(todoReducer, initalTodos);
const filteredTodos = todos.filter(todo => {
if (filter === 'ALL') {
return true;
}
if (filter === 'COMPLETE' && todo.complete) {
return true;
}
if (filter === 'INCOMPLETE' && !todo.complete) {
return true;
}
return false;
});
return (
<TodoContext.Provider value={dispatchTodos}>
<Filter dispatch={dispatchFilter} />
<TodoList todos={filteredTodos} />
<AddTodo />
</TodoContext.Provider>
);
};
const Filter = ({ dispatch }) => {
const handleShowAll = () => {
dispatch({ type: 'SHOW_ALL' });
};
const handleShowComplete = () => {
dispatch({ type: 'SHOW_COMPLETE' });
};
const handleShowIncomplete = () => {
dispatch({ type: 'SHOW_INCOMPLETE' });
};
return (
<div>
<button type="button" onClick={handleShowAll}>
Show All
</button>
<button type="button" onClick={handleShowComplete}>
Show Complete
</button>
<button type="button" onClick={handleShowIncomplete}>
Show Incomplete
</button>
</div>
);
};
const TodoList = ({ todos }) => (
<ul>
{todos.map(todo => (
<TodoItem key={todo.id} todo={todo} />
))}
</ul>
);
const TodoItem = ({ todo }) => {
const dispatch = useContext(TodoContext);
const handleChange = () =>
dispatch({
type: todo.complete ? 'UNDO_TODO' : 'DO_TODO',
id: todo.id,
});
return (
<li>
<label>
<input
type="checkbox"
checked={todo.complete}
onChange={handleChange}
/>
{todo.task}
</label>
</li>
);
};
const AddTodo = () => {
const dispatch = useContext(TodoContext);
const [task, setTask] = useState('');
const handleSubmit = event => {
if (task) {
dispatch({ type: 'ADD_TODO', task });
}
setTask('');
event.preventDefault();
};
const handleChange = event => setTask(event.target.value);
return (
<form onSubmit={handleSubmit}>
<input type="text" value={task} onChange={handleChange} />
<button type="submit">Add Todo</button>
</form>
);
};
export default App;
/*
- Once you move past managing a primitive (e.g. string, integer, boolean)
but rather a complex object (e.g. with arrays and additional primitives),
you may be better of using useReducer to manage this object.
- Good rule of thumb:
* Use useState whenever you manage a JS primitive (e.g. string, boolean, integer).
* Use useReducer whenever you manage an object or array.
* The rule of thumb suggests, for instance, once you spot
const [state, setState] = useState({ firstname: 'Robin', lastname: 'Wieruch' }) in your code,
you may be better off with useReducer instead of useState.
* Once you spot multiple setState() calls in succession,
try to encapsulate these things in one reducer function to dispatch only one action instead.
- Using useReducer over useState gives us predictable state transitions.
*/
import React, { useReducer } from 'react';
const counterReducer = (state, action) => {
switch (action.type) {
case 'INCREASE':
return { ...state, count: state.count + 1 };
case 'DECREASE':
return { ...state, count: state.count - 1 };
default:
throw new Error();
}
};
const Counter = () => {
const [state, dispatch] = useReducer(counterReducer, { count: 0 });
const handleIncrease = () => {
dispatch({ type: 'INCREASE' });
};
const handleDecrease = () => {
dispatch({ type: 'DECREASE' });
};
return (
<div>
<h1>Counter with useReducer</h1>
<p>Count: {state.count}</p>
<div>
<button type="button" onClick={handleIncrease}>
+
</button>
<button type="button" onClick={handleDecrease}>
-
</button>
</div>
</div>
);
};
export default Counter;
/*
- Performing complex state transitions with useState instead useReducer leads to having
all your state relevant logic in your handlers which call the state updater functions from useState eventually.
Over time, it becomes harder to separate state logic from view logic and the components grow in complexity.
Reducers instead offer the perfect place [separation] for logic that alters the state.
*/
import React, { useState } from 'react';
const Counter = () => {
const [count, setCount] = useState(0);
const handleIncrease = () => {
setCount(count => count + 1);
};
const handleDecrease = () => {
setCount(count => count - 1);
};
return (
<div>
<h1>Counter with useState</h1>
<p>Count: {count}</p>
<div>
<button type="button" onClick={handleIncrease}>
+
</button>
<button type="button" onClick={handleDecrease}>
-
</button>
</div>
</div>
);
};
export default Counter;
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment