A trial to achieve a correct approach. Trying to get rid off using Redux, make contexts more useful with useReducer and make components "easy-to-test simple functions".
A highly decoupled, testable TodoList app that uses React hooks.
This is a training repo to learn about new hooks feature of React and creating a testable environment.
- Zero-dependency
- No class components
- Uses
Context
to share a global state - Uses
useReducer
to manage state actions useState
to create local state- Decoupled state logic (Actions)
- Testable components (Uses Jest + Enzyme for tests)
- Custom Hooks for persisting state.
For better approaches please open Pull Requests
The main approach was to get rid off Redux and use React Contexts instead. With the composition of useState
, useContext
I created a global state. And passed it into a custom hook called useTodos
. useTodos
curries useState
output and generates a state manager which will be passed into TodoContext.Provider
to be used as a global state.
function App() {
// create a global store to store the state
const globalStore = useContext(Store);
// `todos` will be a state manager to manage state.
const [state, dispatch] = useReducer(reducer, globalStore);
return (
// State.Provider passes the state and dispatcher to the down
<Store.Provider value={{ state, dispatch }}>
<TodoList />
<TodoForm />
</Store.Provider>
);
}
The second approach was to seperate the main logic, just as the actions of Redux. But these are fully functional, every function returns whole state.
// Reducer is the classical reducer that we know from Redux.
// used by `useReducer`
export default function reducer(state, action) {
switch (action.type) {
case "ADD_TODO":
return {
...state,
todos: [...state.todos, action.payload]
};
case "COMPLETE":
return {
...state,
todos: state.todos.filter(t => t !== action.payload)
};
default:
return state;
}
}
I reach out state and dispathcer of context using useContext
and I can reach to the actions
.
import React, { useContext } from "react";
import Store from "../context";
export default function TodoForm() {
const { state, dispatch } = useContext(Store);
// use `state.todos` to get todos
// use `dispatch({ type: 'ADD_TODO', payload: 'Buy milk' })`
I created custom hooks to persist state on localStorage
import { useEffect } from "react";
// Accepts `useContext` as first parameter and returns cached context.
export function usePersistedContext(context, key = "state") {
const persistedContext = localStorage.getItem(key);
return persistedContext ? JSON.parse(persistedContext) : context;
}
// Accepts `useReducer` as first parameter and returns cached reducer.
export function usePersistedReducer([state, dispatch], key = "state") {
useEffect(() => localStorage.setItem(key, JSON.stringify(state)), [state]);
return [state, dispatch];
}
The App
function will be:
function App () {
const globalStore = usePersistedContext(useContext(Store));
// `todos` will be a state manager to manage state.
const [state, dispatch] = usePersistedReducer(useReducer(reducer, globalStore));
The last but most important part of the approach is to make all the parts testable. They don't tie to eachother which makes me to write tests easily.
MIT