React hooks: dispatch action from useEffect
Asked Answered
B

5

35

My folder structure:

|--App
  |--Components
    |--PageA.js
    |--PageB.js
    |--PageC.js
  |--common-effects
    |--useFetching.js

I am refactoring my code to fetch data from API, using react hooks. I want to dispatch an action from useEffect in useFetching.js that is intercepted by saga middleware. The action should be dispatched only when the components(PageA, PageB, PageC) mount.

I am using redux, react-redux and redux-saga.

PageA.js:

function(props) {
  useFetching(actionParams)
  //....//
}

Similar code for PageB and PageC components.

I have abstracted the reusable code to fetch data in useFetching Custom hook.

useFetching.js

const useFetching = actionArgs => {
  useEffect( () => {
    store.dispatch(action(actionArgs)); // does not work
  })
}

I don't know how to access redux dispatch in useFetching. I tried it with useReducer effect, but the sagas missed the action.

Brucine answered 28/2, 2019 at 16:28 Comment(3)
I don't think hooks are meant to be used in arrow functions like that. Also, if you want to use redux they've got hooks for that: github.com/reduxjs/react-redux/pull/1000Gradient
By arrow function do you mean, const useFetching = actionArgs => {}, if I am not wrong?Brucine
yeah but looks like i was wrongGradient
P
16

You would need to pass either bound action creators or a reference to dispatch to your hook. These would come from a connected component, same as you would normally use React-Redux:

function MyComponent(props) {
    useFetching(props.fetchSomething);

    return <div>Doing some fetching!</div>
}

const mapDispatch = {
    fetchSomething
};

export default connect(null, mapDispatch)(MyComponent);

The hook should then call the bound action creator in the effect, which will dispatch the action accordingly.

Also, note that your current hook will re-run the effect every time the component is re-rendered, rather than just the first time. You'd need to modify the hook like this:

const useFetching = someFetchActionCreator => {
  useEffect( () => {
    someFetchActionCreator();
  }, [])
}
Pudgy answered 28/2, 2019 at 16:51 Comment(5)
All my components use a single action export const fetchData = payload => ({ type: 'FETCH_DATA' }). sagas will then store the value in appropriate reducer of the page by dispatching the corresponding action. According to your solution, I would have to pass reference to dispatch from each component.Brucine
Any action that you want to dispatch, like the fetchData function you just wrote in that comment.Pudgy
I changed my code accordingly, but still saga yield take('FETCH_DATA'); is not able to intercept this actionBrucine
I made some tweaks, it works. Thanks a ton! But I still feel that passing reference to dispatch from every component to custom hook is a bit repeating than what I could in an HOC.Brucine
consider that i don't want connecting action creator to my component specially for my hook. any ideas?Androw
R
46

Version using react-redux hooks:

You can even cut out the connect function completely by using useDispatch from react-redux:

export default function MyComponent() {
  useFetching(fetchSomething);

  return <div>Doing some fetching!</div>
}

with your custom hook

import { useDispatch } from 'react-redux';

const useFetching = (someFetchActionCreator) => {
  const dispatch = useDispatch();
  useEffect(() => {
    dispatch(someFetchActionCreator());
  }, [])
}

Edit: removed dispatch from custom hook as suggested by @yonga-springfield

Note: React guarantees that dispatch function identity is stable and won’t change on re-renders. This is why it’s safe to omit from the useEffect or useCallback dependency list.

Restrictive answered 16/8, 2019 at 20:0 Comment(7)
Should dispatch be included in the dependency array for useEffect?Bartle
I'm getting an error when puting dispatch in the useEffect: React Hook useEffect has a missing dependency: 'dispatch'. Either include it or remove the dependency array react-hooks/exhaustive-depsWeaken
@Bartle techincaly, yes you should. Even though the note there mentions it's identity is stable and won't change; that doesn't mean yo ushoudl omit it and the lint rule if using CRA will continue to throw. More context is described here: github.com/facebook/create-react-app/issues/…Polariscope
"React guarantees that dispatch function identity is stable" source? I could not find this anywhere.Bowknot
@PedroA reactjs.org/docs/…Overmantel
@Overmantel Thanks, but what I see there is that dispatch from useReducer is stable, but what about dispatch from useDispatch from react-redux?Bowknot
https://mcmap.net/q/428869/-what-are-the-cases-where-redux-dispatch-could-changeMapp
P
16

You would need to pass either bound action creators or a reference to dispatch to your hook. These would come from a connected component, same as you would normally use React-Redux:

function MyComponent(props) {
    useFetching(props.fetchSomething);

    return <div>Doing some fetching!</div>
}

const mapDispatch = {
    fetchSomething
};

export default connect(null, mapDispatch)(MyComponent);

The hook should then call the bound action creator in the effect, which will dispatch the action accordingly.

Also, note that your current hook will re-run the effect every time the component is re-rendered, rather than just the first time. You'd need to modify the hook like this:

const useFetching = someFetchActionCreator => {
  useEffect( () => {
    someFetchActionCreator();
  }, [])
}
Pudgy answered 28/2, 2019 at 16:51 Comment(5)
All my components use a single action export const fetchData = payload => ({ type: 'FETCH_DATA' }). sagas will then store the value in appropriate reducer of the page by dispatching the corresponding action. According to your solution, I would have to pass reference to dispatch from each component.Brucine
Any action that you want to dispatch, like the fetchData function you just wrote in that comment.Pudgy
I changed my code accordingly, but still saga yield take('FETCH_DATA'); is not able to intercept this actionBrucine
I made some tweaks, it works. Thanks a ton! But I still feel that passing reference to dispatch from every component to custom hook is a bit repeating than what I could in an HOC.Brucine
consider that i don't want connecting action creator to my component specially for my hook. any ideas?Androw
T
10

This is just to bring some optimization to @Alex Hans' answer.

As per the documentation here. A custom Hook is a JavaScript function whose name starts with ”use” and that may call other Hooks.

With this in mind, we need not send a reference to the dispatch function to the useFetching hook as a parameter but rather, simply not send it and rather simply use it from within the useFetching hook with the appropriate imports.

Here's an excerpt of what I mean.

import { useDispatch } from 'react-redux';

const useFetching = (someFetchActionCreator) => {
    const dispatch = useDispatch()

    useEffect(() => {
        dispatch(someFetchActionCreator());
    }, [])
}

I can't ascertain this example will fit without errors in your codebase in your case but just trying to explain the idea/concept behind this post.

Hope this helps any future comer.

Thrombo answered 23/9, 2019 at 11:44 Comment(1)
it is important to pass dependency parameter of dispatch in the useEffect(() => {...}, [dispatch])Malinin
F
8

Alex Hans right decision with dispatch, but to eliminate request loops to api you can specify the dependence on dispatch ( I used Redux Toolkit )

  import React, { useEffect } from 'react'
  import { useDispatch } from 'react-redux'
  import axios from 'axios'
  import { getItemsStart, getItemsSuccess, getItemsFailure } from '../features/itemsSlice'

  const fetchItems = () => async dispatch => {
    try {
      dispatch(getItemsStart());
      const { data } = await axios.get('url/api')
      dispatch(getItemsSuccess(data))
    } catch (error) {
      dispatch(getItemsFailure(error))
    }
  }

  const PageA = () => {
    const dispatch = useDispatch()
    const { items } = useSelector(state => state.dataSlice)
   
    useEffect(() => {
       dispatch(fetchItems())
    }, [dispatch])

    return (
      <ul>
         {items.map(item => <li>{item.name}</li>}
      </ul> 
    )
  }
  
  export default PageA

it is important to passed dependency parameter of dispatch in the useEffect(() => {...}, [dispatch])

Forestaysail answered 28/6, 2020 at 14:38 Comment(0)
V
1
useEffect(() => {
   fetchData();
 }, []);

 async function fetchData() {
   try {
     await Auth.currentSession();
     userHasAuthenticated(true);
   } catch (e) {
     if (e !== "No current user") {
       alert(e);
     }
   }
   dispatch(authentication({ type: "SET_AUTHING", payload: false }));
 }
Voice answered 9/2, 2020 at 17:34 Comment(1)
Welcome to Stack Overflow! Consider adding some explanation as to how/why your posted code works.Tobey

© 2022 - 2024 — McMap. All rights reserved.