Search code examples
javascriptreactjsredux

Redux with Nextjs - Error: could not find react-redux context value; please ensure the component is wrapped in a <Provider>


I am fetching to get data from the server before the app is rendered, by store that data into the redux store.

And facing this error:

Error: could not find react-redux context value; please ensure the component is wrapped in a

in _app.tsx:

function MyApp({ Component, pageProps, data }: AppProps & { data: any }) {
  console.log(data); // data is here

  const dispatch = useDispatch();

  useEffect(() => {
    // Dispatch an action to store the fetched data in Redux store
    dispatch(saveData(data));
  }, [dispatch]);

  return (
    <Provider store={store}>
      <Component {...pageProps} />
    </Provider>
  );
}

MyApp.getInitialProps = async ({ Component, ctx }) => {
  const data = await fetch(`http://base-url/my-route`).then((res) => res.json());

  let pageProps = {};

  if (Component.getInitialProps) {
    pageProps = await Component.getInitialProps(ctx);
  }

  return { pageProps, data };
};

export default MyApp;

store is exported from store.ts, and I am able to use those useDispatch and useSelector well in other parts of the app.


Solution

  • You can not use useDispatch outside the Provider.

    You can use useDispatch inside your Component, but not in MyApp, because Component is inside the Provider with the connected store, but MyApp is not:

    function MyApp() {
    
      // -- here is outside the Provider (no useDispatch possible) --
    
        return (
            <Provider store={ store }>
                {/* -- here is inside the provider -- */}
                <Component {...pageProps} />
            </Provider>
        );
    }
    

    There are 3 solutions:

    1. Add the data during the creation of the initial store object
    2. Use the store object directly (without useDispatch)
    3. Create a Loader component, which can use useDispatch

    1. Add the data during the creation of the initial store object

    You surely have code to create the store somewhere, e.g.:

    const myStore = createStore( myReducer, myInitialState );
    

    You might be able to create the initial state myInitialState with your data from getInitialProps already included:

    const myStore = useMemo(() => {
        const staticInitialState = {
            // your static state
        };
        
        const myInitialState = {
            ...staticInitialState,
            ...dataFromGetInitialProps
        }; 
        
        return createStore( reducer, myInitialState );
    }, []);
    

    I think this would be the cleanest solution, because it is exactly inline with both the React and the Redux concepts.

    (Apparently createStore is deprecated and there is a new, much more complicated syntax with configureStore and createSlice, which I'm not yer familiar with.)

    2. Use the store object directly

    You can not use useDispatch inside MyApp, but you do have the store object available there. The store object has a dispatch and a getState method, which you can use, e.g.:

    function MyApp() {
    
        console.log( store.getState() );
    
        useEffect(() => {
            store.dispatch( saveData( data ) );
        }, []);
        
        return (
            <Provider store={ store }>
                <Component {...pageProps} />
            </Provider>
        );
    }
    

    I think this is ok, but more like a "workaround" than a "solution", because using the store methods directly seems a bit more low level than using the Provider, and probably intended for more advanced use cases.

    3. Create a 'Loader' component

    You can create a component inside the Provider, and put your dispatch logic there. Alternatively you could create a Wrapper HOC around the Component, if more appropriate.

    function DataLoader({ data }){
    
        const dispatch = useDispatch();
    
        useEffect( () => {
            dispatch( saveData( data ) );
        }, [] );
    
        return null;
    }
    
    function MyApp({ Component, pageProps, data }){
        return (
            <Provider store={ store }>
                <DataLoader data={ data } />
                <Component { ...pageProps } />
            </Provider>
        );
    }
    

    This seems pretty hacky to me, because is (mis-)uses a component as a kind of "life cycle hook", and there is no logical relation between the DataLoader and the location where it is put inside the code (it could really be anywhere, as long as the component is rendered at the right time).