A highly testable TodoList app that uses React hooks and Context

Nov 06, 2018
A highly testable TodoList app that uses React hooks and Context

React Hooks Todo App

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".

View demo Download Source

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


1. Context:

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> ); }

2. The Reducer:

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; } }

3. State and Dispatcher

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' })`

4. Persistence with custom hooks:

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));

5. Everything is testable decoupled:

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.


Related React stuff