Skip to content

Instantly share code, notes, and snippets.

@astoilkov
Last active November 16, 2024 12:52

Revisions

  1. Antonio Stoilkov revised this gist Jul 7, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -1,6 +1,6 @@
    # Async Operations with `useReducer` Hook

    **9 Mar, 2019**
    **9 March, 2019**

    We were discussing with [@erusev](https://github.com/erusev) what we can do with async operation when using `useReducer()` in our application. Our app is simple and we don't want to use a state management library. All our requirements are satisfied with using one root `useReducer()`. The problem we are facing and don't know how to solve is async operations.

  2. Antonio Stoilkov revised this gist Jul 7, 2019. 1 changed file with 2 additions and 0 deletions.
    2 changes: 2 additions & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -1,5 +1,7 @@
    # Async Operations with `useReducer` Hook

    **9 Mar, 2019**

    We were discussing with [@erusev](https://github.com/erusev) what we can do with async operation when using `useReducer()` in our application. Our app is simple and we don't want to use a state management library. All our requirements are satisfied with using one root `useReducer()`. The problem we are facing and don't know how to solve is async operations.

    In [a discussion with Dan Abramov](https://twitter.com/antoniostoilkov/status/1104049838113636352) he recommends **Solution 3** but points out that things are fresh with hooks and there could be better ways of handling the problem.
  3. Antonio Stoilkov revised this gist Mar 9, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -1,4 +1,4 @@
    # async operations with `useReducer` hook
    # Async Operations with `useReducer` Hook

    We were discussing with [@erusev](https://github.com/erusev) what we can do with async operation when using `useReducer()` in our application. Our app is simple and we don't want to use a state management library. All our requirements are satisfied with using one root `useReducer()`. The problem we are facing and don't know how to solve is async operations.

  4. Antonio Stoilkov revised this gist Mar 9, 2019. 1 changed file with 3 additions and 1 deletion.
    4 changes: 3 additions & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -1,7 +1,9 @@
    # async operations using `useReducer`
    # async operations with `useReducer` hook

    We were discussing with [@erusev](https://github.com/erusev) what we can do with async operation when using `useReducer()` in our application. Our app is simple and we don't want to use a state management library. All our requirements are satisfied with using one root `useReducer()`. The problem we are facing and don't know how to solve is async operations.

    In [a discussion with Dan Abramov](https://twitter.com/antoniostoilkov/status/1104049838113636352) he recommends **Solution 3** but points out that things are fresh with hooks and there could be better ways of handling the problem.

    ## Problem

    Doing asynchronous operations in a `useReducer` reducer is not possible. We have thought of three possible solutions and can't figure which one is better or if there is an even better solution.
  5. Antonio Stoilkov revised this gist Mar 8, 2019. 1 changed file with 1 addition and 1 deletion.
    2 changes: 1 addition & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -191,6 +191,6 @@ function Component({ files }) {

    ## Conclusion

    It seems we have two questions we are not sure the answers to:
    While discussing we raised two questions we are not sure the answers to. They can help us in deciding the right pattern:
    - Is it normal to do side effects like writing a file or fetching data in an event handler or it should be only in `useEffect()`?
    - Is it normal to have properties in the state object which are not used in the view at all?
  6. Antonio Stoilkov revised this gist Mar 8, 2019. 1 changed file with 2 additions and 2 deletions.
    4 changes: 2 additions & 2 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -73,7 +73,7 @@ Use `useEffect()` hook to delete the file.
    ### Cons
    - An additional state property
    - State property which may not be used in the UI. Not sure if that is a problem. Maybe state properties that are not part of the UI are normal.
    - If while deleting the file we don't want to show an UI indication the code goes through the component twice which is a small inefficiency and a confusement when you imagine it. Goes through the component logic just to execute a `useEffect()` call.
    - If while deleting the file we don't want to show a UI indication the code goes through the component twice which is a small inefficiency and a confusion when you imagine it. Goes through the component logic just to execute a `useEffect()` call.

    ```jsx
    function App() {
    @@ -192,5 +192,5 @@ function Component({ files }) {
    ## Conclusion

    It seems we have two questions we are not sure the answers to:
    - Is it normal to do side effects like writing file or fetching data in an event handler or it should be only in `useEffect()`?
    - Is it normal to do side effects like writing a file or fetching data in an event handler or it should be only in `useEffect()`?
    - Is it normal to have properties in the state object which are not used in the view at all?
  7. Antonio Stoilkov revised this gist Mar 8, 2019. 1 changed file with 7 additions and 1 deletion.
    8 changes: 7 additions & 1 deletion readme.md
    Original file line number Diff line number Diff line change
    @@ -187,4 +187,10 @@ function Component({ files }) {
    </>
    );
    }
    ```
    ```

    ## Conclusion

    It seems we have two questions we are not sure the answers to:
    - Is it normal to do side effects like writing file or fetching data in an event handler or it should be only in `useEffect()`?
    - Is it normal to have properties in the state object which are not used in the view at all?
  8. Antonio Stoilkov revised this gist Mar 8, 2019. 1 changed file with 13 additions and 5 deletions.
    18 changes: 13 additions & 5 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -71,20 +71,26 @@ Use `useEffect()` hook to delete the file.
    - The only location where a side effect like writing to a file or fetching data can be is in a `useEffect()` hook. This improves the cognitive load of understanding the code.

    ### Cons
    - What happens if an error occurs. You already changed the state. What are you going to do? Revert it? This seems complicated.
    - An additional state property
    - State property which may not be used in the UI. Not sure if that is a problem. Maybe state properties that are not part of the UI are normal.
    - If while deleting the file we don't want to show an UI indication the code goes through the component twice which is a small inefficiency and a confusement when you imagine it. Goes through the component logic just to execute a `useEffect()` call.

    ```jsx
    function App() {
    const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
    case 'REQUEST_DELETE_FILE':
    return {
    ...state,

    deleteFile: action.file
    };
    case 'DELETE_FILE':
    let index = state.files.indexOf(action.file);
    const index = state.files.indexOf(action.file);

    return {
    ...state,

    deleteFile: action.file,
    files: [...state.files.slice(0, index), ...state.file.slice(index + 1)]
    };
    }
    @@ -98,7 +104,9 @@ function App() {
    return;
    }

    unlink(state.deleteFile, () => { });
    unlink(state.deleteFile, () => {
    dispatch({ type: 'DELETE_FILE', file: state.deleteFile });
    });
    }, [state.deleteFile]);

    return (
    @@ -114,7 +122,7 @@ function Component({ files }) {
    return (
    <>
    {files.map(file =>
    <button onClick={dispatch({ type: 'DELETE_FILE', file: file })}>Delete File</button>
    <button onClick={dispatch({ type: 'REQUEST_DELETE_FILE', file: file })}>Delete File</button>
    )}
    </>
    );
  9. Antonio Stoilkov revised this gist Mar 8, 2019. 1 changed file with 3 additions and 3 deletions.
    6 changes: 3 additions & 3 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -1,12 +1,12 @@
    # async operations using `useReducer`

    We were discussing with [@erusev](https://github.com/erusev) what we can do when we have async operation that we should do when using `useReducer()` in our application. Our app is simple and we don't want to use a state management library. All our requirements are satisfied with using one root `useReducer()`. The problem we are facing and don't know how to solve is async operations.
    We were discussing with [@erusev](https://github.com/erusev) what we can do with async operation when using `useReducer()` in our application. Our app is simple and we don't want to use a state management library. All our requirements are satisfied with using one root `useReducer()`. The problem we are facing and don't know how to solve is async operations.

    ## Problem

    Doing asynchronous operations in a `useReducer` reducer is not possible. We should find a way to do it using a new concept. We have thought of two possible solutions and can't figure which one is better or if there is an even better solution.
    Doing asynchronous operations in a `useReducer` reducer is not possible. We have thought of three possible solutions and can't figure which one is better or if there is an even better solution.

    We are searching for a solution which will be used multiple times in multiple places in the application.
    We are searching for a solution where a single action will be used multiple times in multiple places all over the application.

    ## Solution 1

  10. Antonio Stoilkov created this gist Mar 8, 2019.
    182 changes: 182 additions & 0 deletions readme.md
    Original file line number Diff line number Diff line change
    @@ -0,0 +1,182 @@
    # async operations using `useReducer`

    We were discussing with [@erusev](https://github.com/erusev) what we can do when we have async operation that we should do when using `useReducer()` in our application. Our app is simple and we don't want to use a state management library. All our requirements are satisfied with using one root `useReducer()`. The problem we are facing and don't know how to solve is async operations.

    ## Problem

    Doing asynchronous operations in a `useReducer` reducer is not possible. We should find a way to do it using a new concept. We have thought of two possible solutions and can't figure which one is better or if there is an even better solution.

    We are searching for a solution which will be used multiple times in multiple places in the application.

    ## Solution 1

    Just manually call the async function and after it completes call the dispatch.

    ### Pros
    - No additional abstraction
    - Doesn't introduce additional learning curve because it uses already existing ideas

    ### Cons
    - Now calling `dispatch({ type: 'DELETE_FILE' })` have an invisible dependency/requirement. If you don't execute the required code before the `dispatch` call you are calling for a strange bug that can be missed depending on the app architecture
    - For larger async operations we need to extract the code in a global place

    ```jsx
    function App() {
    const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
    case 'DELETE_FILE':
    let index = state.files.indexOf(action.file);

    return {
    ...state,

    files: [...state.files.slice(0, index), ...state.file.slice(index + 1)]
    };
    }
    }, {
    files: ['a', 'b', 'c'],
    });

    return (
    <DispatchContext.Provider value={dispatchMiddleware(dispatch)}>
    <Component files={state.files} />
    </DispatchContext.Provider>
    );
    }

    function Component({ files }) {
    const dispatch = useContext(DispatchContext);

    function deleteFile(file) {
    unlink(file, () => {
    dispatch({ type: 'DELETE_FILE', file: file });
    });
    }

    return (
    <>
    {files.map(file =>
    <button onClick={() => deleteFile(file)}>Delete File</button>
    )}
    </>
    );
    }
    ```

    ## Solution 2

    Use `useEffect()` hook to delete the file.

    ### Pros
    - The only location where a side effect like writing to a file or fetching data can be is in a `useEffect()` hook. This improves the cognitive load of understanding the code.

    ### Cons
    - What happens if an error occurs. You already changed the state. What are you going to do? Revert it? This seems complicated.
    - An additional state property

    ```jsx
    function App() {
    const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
    case 'DELETE_FILE':
    let index = state.files.indexOf(action.file);

    return {
    ...state,

    deleteFile: action.file,
    files: [...state.files.slice(0, index), ...state.file.slice(index + 1)]
    };
    }
    }, {
    deleteFile: null,
    files: ['a', 'b', 'c'],
    });

    useEffect(() => {
    if (!state.deleteFile) {
    return;
    }

    unlink(state.deleteFile, () => { });
    }, [state.deleteFile]);

    return (
    <DispatchContext.Provider value={dispatchMiddleware(dispatch)}>
    <Component files={state.files} />
    </DispatchContext.Provider>
    );
    }

    function Component({ files }) {
    const dispatch = useContext(DispatchContext);

    return (
    <>
    {files.map(file =>
    <button onClick={dispatch({ type: 'DELETE_FILE', file: file })}>Delete File</button>
    )}
    </>
    );
    }
    ```

    ## Solution 3

    Use a middleware for dispatch which performs the async operation and then calls the actual dispatch.

    ### Pros
    - Doesn't have the disadvantages of **Solution 1** and **Solution 2**

    ### Cons
    - A more complicated architecture. Two places where actions are handled.

    ```jsx
    function dispatchMiddleware(dispatch) {
    return (action) => {
    switch (action.type) {
    case 'DELETE_FILE':
    unlink(action.file, () => dispatch(action));
    break;

    default:
    return dispatch(action);
    }
    };
    }

    function App() {
    const [state, dispatch] = useReducer((state, action) => {
    switch (action.type) {
    case 'DELETE_FILE':
    let index = state.files.indexOf(action.file);

    return {
    ...state,

    files: [...state.files.slice(0, index), ...state.file.slice(index + 1)]
    };
    }
    }, {
    files: ['a', 'b', 'c']
    });

    return (
    <DispatchContext.Provider value={dispatchMiddleware(dispatch)}>
    <Component files={state.files} />
    </DispatchContext.Provider>
    );
    }

    function Component({ files }) {
    const dispatch = useContext(DispatchContext);

    return (
    <>
    {files.map(file =>
    <button onClick={dispatch({ type: 'DELETE_FILE', file: file })}>Delete File</button>
    )}
    </>
    );
    }
    ```