'
36 | );
37 | });
38 |
39 | test(" #completeCalls", async () => {
40 | const todos = ["a", "b", "c"];
41 | const dispatch = jest.fn();
42 | const list = mount(
43 |
44 |
45 |
46 | );
47 |
48 | list.find("button").forEach(b => b.simulate("click"));
49 | expect(dispatch.mock.calls.length).toBe(3);
50 | });
51 |
52 | test(" #completeMutates", async () => {
53 | let state = { todos: ["a", "b", "c"] };
54 | const dispatch = action => {
55 | state = reducer(state, action);
56 | };
57 | const list = mount(
58 |
59 |
60 |
61 | );
62 |
63 | await list
64 | .find("button")
65 | .last()
66 | .simulate("click");
67 | expect(state.todos.length).toBe(2);
68 | expect(state.todos).toEqual(["a", "b"]);
69 | });
70 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # React Hooks Todo App
2 |
3 | > 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"**.
4 |
5 | [](https://codesandbox.io/s/github/f/react-hooks-todo-app/tree/master/)
6 |
7 | ---
8 |
9 | A **highly decoupled**, **testable** TodoList app that uses React hooks.
10 |
11 | This is a training repo to learn about new hooks feature of React and creating a testable environment.
12 |
13 | - **Zero-dependency**
14 | - **No** class components
15 | - Uses `Context` to share a **global state**
16 | - Uses `useReducer` to manage **state actions**
17 | - `useState` to create local state
18 | - Decoupled state logic (Actions)
19 | - Testable components (Uses Jest + Enzyme for tests)
20 | - Custom Hooks for **persisting state**.
21 |
22 | For better approaches please open Pull Requests
23 |
24 | ## Summary
25 |
26 | ### 1. **Context**:
27 |
28 | 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.
29 |
30 | ```jsx
31 | function App() {
32 | // create a global store to store the state
33 | const globalStore = useContext(Store);
34 |
35 | // `todos` will be a state manager to manage state.
36 | const [state, dispatch] = useReducer(reducer, globalStore);
37 |
38 | return (
39 | // State.Provider passes the state and dispatcher to the down
40 |
41 |
42 |
43 |
44 | );
45 | }
46 | ```
47 |
48 | ### 2. **The Reducer**:
49 |
50 | 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.
51 |
52 | ```js
53 | // Reducer is the classical reducer that we know from Redux.
54 | // used by `useReducer`
55 | export default function reducer(state, action) {
56 | switch (action.type) {
57 | case "ADD_TODO":
58 | return {
59 | ...state,
60 | todos: [...state.todos, action.payload]
61 | };
62 | case "COMPLETE":
63 | return {
64 | ...state,
65 | todos: state.todos.filter(t => t !== action.payload)
66 | };
67 | default:
68 | return state;
69 | }
70 | }
71 | ```
72 |
73 | ### 3. **State and Dispatcher**
74 |
75 | I reach out **state and dispathcer** of context using `useContext` and I can reach to the `actions`.
76 |
77 | ```js
78 | import React, { useContext } from "react";
79 | import Store from "../context";
80 |
81 | export default function TodoForm() {
82 | const { state, dispatch } = useContext(Store);
83 | // use `state.todos` to get todos
84 | // use `dispatch({ type: 'ADD_TODO', payload: 'Buy milk' })`
85 | ```
86 |
87 | ### 4. **Persistence with custom hooks**:
88 |
89 | I created custom hooks to persist state on `localStorage`
90 |
91 | ```js
92 | import { useEffect } from "react";
93 |
94 | // Accepts `useContext` as first parameter and returns cached context.
95 | export function usePersistedContext(context, key = "state") {
96 | const persistedContext = localStorage.getItem(key);
97 | return persistedContext ? JSON.parse(persistedContext) : context;
98 | }
99 |
100 | // Accepts `useReducer` as first parameter and returns cached reducer.
101 | export function usePersistedReducer([state, dispatch], key = "state") {
102 | useEffect(() => localStorage.setItem(key, JSON.stringify(state)), [state]);
103 | return [state, dispatch];
104 | }
105 | ```
106 |
107 | The `App` function will be:
108 |
109 | ```js
110 | function App () {
111 | const globalStore = usePersistedContext(useContext(Store));
112 |
113 | // `todos` will be a state manager to manage state.
114 | const [state, dispatch] = usePersistedReducer(useReducer(reducer, globalStore));
115 | ```
116 |
117 | ### 5. **Everything is testable decoupled**:
118 |
119 | 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.
120 |
121 | ## License
122 | MIT
123 |
--------------------------------------------------------------------------------