Jithin Varghese
Jithin Varghese

Reputation: 2228

NEXT.JS with persist redux showing unexpected behaviour

I have a ecommerce store which list out all products and filter option. Today I have added login and regsiter options. So to get saved token even after page refresh, I have implemented redux-persist. Now the problem is, if I refresh the page the design is breaking and state is not getting. In product listing page I have placed a loader before fetching and loader will be hide after products loaded. Now every time loader is showing with some other animation. Please have a look into my code,

store.js

import { createWrapper } from 'next-redux-wrapper';
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
import rootReducer from './reducers';

const bindMiddleware = (middleware) => {
    if (process.env.NODE_ENV !== "production") {
        const { composeWithDevTools } = require("redux-devtools-extension");
        return composeWithDevTools(applyMiddleware(...middleware));
    }
    return applyMiddleware(...middleware);
};

const makeStore = ({ isServer }) => {
    if (isServer) {
        //If it's on server side, create a store
        return createStore(rootReducer, bindMiddleware([thunkMiddleware, logger]));
    } else {
        //If it's on client side, create a store which will persist
        const { persistStore, persistReducer, autoRehydrate } = require("redux-persist");
        const storage = require("redux-persist/lib/storage").default;

        const persistConfig = {
            key: "nextjs",
            whitelist: ["authentication", "menu", "product"], // only counter will be persisted, add other reducers if needed
            storage, // if needed, use a safer storage
        };

        const persistedReducer = persistReducer(persistConfig, rootReducer); // Create a new reducer with our existing reducer

        const store = createStore(
            persistedReducer,
            {},
            bindMiddleware([thunkMiddleware, logger])
        ); // Creating the store again

        store.__persistor = persistStore(store); // This creates a persistor object & push that persisted object to .__persistor, so that we can avail the persistability feature

        return store;
    }
};

// Export the wrapper & wrap the pages/_app.js with this wrapper only
export const wrapper = createWrapper(makeStore);

[...slug].js

import { useRouter, withRouter } from 'next/router'
import { useDispatch, useSelector } from 'react-redux';
import { useEffect, useState } from 'react';
import { Layout } from '../../components/main/Index';
import { fetchproducts } from '../../store/actions/productAction'
import './styles/listing.scss';
import Link from 'next/link';
import {FontAwesomeIcon} from '@fortawesome/react-fontawesome'
import { faTimes, faChevronDown, faChevronUp, faHeart as heartBold, faShoppingCart } from '@fortawesome/fontawesome-free-solid'
import { faHeart as heartRegular } from '@fortawesome/fontawesome-free-regular'
import ReactPaginate from 'react-paginate';
import * as api from '../api'
import Head from 'next/head';

const Products = () => {
    const {products, loading} = useSelector(state => state.product);
    const [showMe, setShowMe] = useState([0]);
    const [showFilter, setFilter] = useState([]);
    const [yourSelection, setSelection] = useState([]);
    let [sortState, setSortState] = useState(0);
    let [pageState, setPageState] = useState(0);
    const router = useRouter()
    const dispatch = useDispatch();

    const slug = router.query.slug || []
    const sort = router.query.sort || []
    const page = router.query.page || []
    const pageInitial = router.query.page ? parseInt(page-1) : pageState;

    useEffect(() => {
        dispatch(fetchproducts(slug, sort, pageInitial+1, showFilter));
    }, [showFilter]);

    function toggle(index) {
        setShowMe(currentShowMe => currentShowMe.includes(index) ? currentShowMe.filter(i => i !== index) : [...currentShowMe, index]);
    }

    function filterClick (id, title) {
        const index = showFilter.indexOf(id);

        if (index > -1)
            setFilter(showFilter.filter(el=> el !== id));
        else
            setFilter(showFilter.concat(id));

        setPageState(0);
        setSortState(0);

        router.push(
            '/products/[...slug]',
            {
                pathname: '/products/'+slug.join('/')
            },
            '/products/'+slug.join('/'), { shallow: true }
        )
    }

    function sortBy(value) {
        setPageState(0);
        setSortState(value);
        dispatch(fetchproducts(slug, value, 1, showFilter));

        if (value === "0")
            router.push(
                '/products/[...slug]',
                {
                    pathname: '/products/'+slug.join('/')
                },
                '/products/'+slug.join('/'), { shallow: true }
            )
        else
            router.push(
                '/products/[...slug]',
                {
                    pathname: '/products/'+slug.join('/'),
                    query: { sort: value }
                },
                '/products/'+slug.join('/'), { shallow: true }
            )
    }

    return (
        <Layout title={products && products.title.length > 0 ? products.title : slug.join('-')}>
            <Head>
                <meta name="description" content={products && products.description.length > 0 ? products.description : slug.join('-')} />
            </Head>
            {!loading ?
                <div className="container-fluid mt-4 mb-4">
                    {products && products.data.length > 0 ?
                        <div className="row">
                            <div className="col-md-3">
                                <div className="filter">
                                    <div className="other">
                                        <h6>Refine</h6>
                                        <hr/>
                                        {products.filter.map((item, index) => (
                                            <div key={index}>
                                                <div className="single">
                                                    <div className="title" onClick={() => toggle(index)}>
                                                        <p className="float-left">{item.title}</p>
                                                        <p className="float-right"><FontAwesomeIcon icon={showMe.includes(index) ? faChevronUp : faChevronDown}/></p>
                                                    </div>
                                                    <ul style={{display: showMe.includes(index) ? "block" : "none"}}>
                                                        {item.items.map((single, index1) => (
                                                            <li key={index1}>
                                                                <label><input type="checkbox" name="checkbox" onClick={(e) => filterClick(e.target.value, item.title)} value={single.items_id} {...showFilter.includes(index) ? defaultChecked : "" }/> {single.items_value.charAt(0) === "#" ? <span className="color-filter" style={{backgroundColor: single.items_value}}></span> : single.items_value}</label>
                                                            </li>
                                                        ))}
                                                    </ul>
                                                </div>
                                                <hr/>
                                            </div>
                                        ))}
                                    </div>
                                </div>
                            </div>
                            <div className="col-md-9 mt-4 mt-md-0">
                                <div className="row align-items-center mb-4">
                                    <div className="col-md-9">
                                        <h4 className="m-0">{products && products.heading.length > 0 ? products.heading : slug.join('/')}</h4>
                                    </div>
                                    <div className="col-md-3">
                                        <select id="sort" className="sort-by form-control" onChange={(e) => sortBy(e.target.value)} value={sort.length > 0 ? sort : sortState}>
                                            <option value="0">Sort By</option>
                                            <option value="1">Price Low to High</option>
                                            <option value="2">Price High to Low</option>
                                            <option value="3">New Arrivals</option>
                                        </select>
                                    </div>
                                </div>
                                <div className="row">
                                    {products.data.map((items, index) => (
                                        <div className="col-sm-6 col-md-4" key={index}>
                                            <div className="single-box">
                                                <Link href={"/product/"+items.slug}>
                                                    <a title={items.name}>
                                                        {items.images.length > 0 ?
                                                            <img src={api.IMAGE_PRODUCTS+"/"+items.proId+"/"+items.images[0].image_name} alt={items.name} className="img-fluid" />
                                                        :
                                                            <img src="/images/noimage.png" alt={items.name} className="img-fluid" />
                                                        }
                                                        <div className="details">
                                                            <p className="title">{items.name}</p>
                                                            {items.ofprice !== null ?
                                                                <p>Rs.{items.ofprice} <span className="off-price">Rs.{items.oprice}</span> <span className="off">{parseInt(((items.oprice-items.ofprice)*100)/items.oprice)}% off</span></p>
                                                            :
                                                                <p>Rs.{items.oprice}</p>
                                                            }
                                                        </div>
                                                    </a>
                                                </Link>
                                                <p className={items.wishlist === 1 ? "wishlist w-active" : "wishlist"}><FontAwesomeIcon icon={items.wishlist === 1 ? heartBold : heartRegular}/></p>
                                            </div>
                                        </div>
                                    ))}
                                </div>
                            </div>
                        </div>
                    :
                        <div className="row justify-content-center">
                            <div className="col-12 col-md-6 col-lg-4">
                                <div className="empty-list">
                                    <FontAwesomeIcon icon={faShoppingCart}/>
                                    <h3>Empty</h3>
                                    <p>No products available in this category</p>
                                </div>
                            </div>
                        </div>
                    }
                </div>
            :
                <div className="loading"><div className="lds-ripple"><div></div><div></div></div></div>
            }
        </Layout>
    )
}

export default withRouter(Products)

productAction.js

import * as types from '../types'
import axios from 'axios'
import * as api from '../../pages/api'

export const fetchproducts = (slug, sort = 0, page = 1, filter = null) => async dispatch => {
    const res = await axios.get(api.URL_PRODUCTS+"?slug="+slug+"&user=1&sort="+sort+"&page="+page+"&filter="+filter);
    dispatch({
        type: types.GET_PRODUCTS,
        payload: res.data
    })
}

rootReducer.js

import {combineReducers} from 'redux'
import { authReducer } from './authReducer';
import { menuReducer } from './menuReducer';
import { productReducer } from './productReducer';
import { HYDRATE } from 'next-redux-wrapper';

const reducer = (state = { app: 'init', page: 'init' }, action) => {
    switch (action.type) {
        case HYDRATE:
            if (action.payload.app === 'init') delete action.payload.app;
            if (action.payload.page === 'init') delete action.payload.page;
            return state;
        case 'APP':
            return { ...state, app: action.payload };
        case 'PAGE':
            return { ...state, page: action.payload };
        default:
            return state;
    }
};

const rootReducer = combineReducers({
    wrapper: reducer,
    authentication: authReducer,
    menu: menuReducer,
    product: productReducer
});

export default rootReducer;

_app.js

import App from 'next/app'
import React from 'react'
import {Provider} from 'react-redux'
import {createWrapper} from 'next-redux-wrapper'
import store from '../store/store'
import { PersistGate } from 'redux-persist/integration/react'

class MyApp extends App {
    render() {
        const {Component, pageProps} = this.props

        return (
            <Provider store={store}>
                <PersistGate persistor={store.__persistor} loading={<div>Loading...</div>}>
                    <Component {...pageProps}/>
                </PersistGate>
            </Provider>
        )
    }
}

const makestore = () => store;
const wrapper = createWrapper(makestore);

export default wrapper.withRedux(MyApp);

authReducer.js

import * as types from '../types'

export const authReducer = (state = { token: null }, action) => {
    switch (action.type) {
        case 'persist/REHYDRATE': {
            const data = action.payload;
            if (data) {
                return {
                    ...state,
                    ...data.auth
                }
            }
        }
        case types.AUTHENTICATE:
            return {
                ...state,
                token: action.payload
            };
        case types.DEAUTHENTICATE:
            return {
                token: null
            };
        default:
            return state;
    }
};

Is there any way to fix this. I think my code should be altered to fix this. I have no idea how to fix this, as am new to NEXT.JS and REDUX.

Please check the below video to get an idea of exact problem.

Error video

enter image description here

authAction.js

import * as types from '../types'
import axios from 'axios'
import cookie from 'js-cookie';
import * as api from '../../pages/api'
import Router from 'next/router';

export const authenticate = user => async dispatch => {
    const res = await axios.post(api.URL_REGISTER, {user})
        .then(res => {
            if (res.data.response === 200) {
                setCookie('token', res.data.data.token);
                Router.push('/');

                dispatch({
                    type: types.AUTHENTICATE,
                    payload: res.data.data.token
                })
            }
            else
                dispatch({
                    type: types.AUTHENTICATE,
                    payload: res.data
                })
        }).catch(error => {
            console.log(error);
        });
}

Upvotes: 1

Views: 4558

Answers (1)

Vagan M.
Vagan M.

Reputation: 463

My working store in Next.js app
Try to fix your store

store.js

import { createWrapper } from 'next-redux-wrapper';
import { applyMiddleware, createStore } from 'redux';
import logger from 'redux-logger';
import thunkMiddleware from 'redux-thunk';
import rootReducer from './reducers/rootReducer';

const bindMiddleware = (middleware) => {
    if (process.env.NODE_ENV !== "production") {
        const { composeWithDevTools } = require("redux-devtools-extension");
        return composeWithDevTools(applyMiddleware(...middleware));
    }
    return applyMiddleware(...middleware);
};

const makeStore = ({ isServer }) => {
    if (isServer) {
        //If it's on server side, create a store
        return createStore(rootReducer, bindMiddleware([thunkMiddleware, logger]));
    } else {
        //If it's on client side, create a store which will persist
        const { persistStore, persistReducer, autoRehydrate } = require("redux-persist");
        const storage = require("redux-persist/lib/storage").default;

        const persistConfig = {
            key: "nextjs",
            whitelist: ["auth","cart"], // only counter will be persisted, add other reducers if needed
            storage, // if needed, use a safer storage
        };

        const persistedReducer = persistReducer(persistConfig, rootReducer); // Create a new reducer with our existing reducer

        const store = createStore(
            persistedReducer,
            {},
            bindMiddleware([thunkMiddleware, logger])
        ); // Creating the store again

        store.__persistor = persistStore(store); // This creates a persistor object & push that persisted object to .__persistor, so that we can avail the persistability feature

        return store;
    }
};

// Export the wrapper & wrap the pages/_app.js with this wrapper only
export const wrapper = createWrapper(makeStore);

Root reducer

const reducer = (state = { app: 'init', page: 'init' }, action) => {
    switch (action.type) {
        case HYDRATE:
            if (action.payload.app === 'init') delete action.payload.app;
            if (action.payload.page === 'init') delete action.payload.page;
            return state;
        case 'APP':
            return { ...state, app: action.payload };
        case 'PAGE':
            return { ...state, page: action.payload };
        default:
            return state;
    }
};


const rootReducer = combineReducers({
    wrapper: reducer,
    cart: cartReducer,
    auth: authReducer
});

auth.js for example

const authReducer = (state = initialState, action) => {
    switch (action.type) {
       ...
        case 'persist/REHYDRATE': {
            const data = action.payload;
            if (data) {
                return {
                    ...state,
                    ...data.auth
                }
            }
        }
       ...
    }
};

_app.js

import React from 'react';
import { useStore } from "react-redux";
import { PersistGate } from "redux-persist/integration/react";
import { wrapper } from '../store/store';

const Layout = ({ children }) => {
  return <>
    <Header />
    <div className="container">
      {children}
    </div>
  </>
}


const App = ({ Component, pageProps }) => {
  const store = useStore((state) => state)
  return (
    <Layout >
      <PersistGate persistor={store.__persistor} loading={<div>Loading...</div>}>
        <Component {...pageProps} />
      </PersistGate>
      <style jsx global>{`
        html,
        body {
          padding: 0;
          margin: 0;
          font-family: 'Montserrat', sans-serif;
        }
        .container { 
          width:1240px;
          margin: 0 auto;
        }
        
        img,li,ul,ol,span,h1,h2,h3,h4,h5,h6,main,footer {
          margin:0;
          padding:0;
        }

        main {
          width:100%
        }

        *,*::before,*::after {
          box-sizing: border-box;
        }
        a,
        button {
          cursor: pointer;
        } 

        a {
          text-decoration:none;
          color:#000
        }
        
      `}</style>
    </Layout>
  )
}

App.getInitialProps = async ({ Component, ctx }) => {
  // Keep in mind that this will be called twice on server, one for page and second for error page
  ctx.store.dispatch({ type: "APP", payload: "was set in _app" });
  return {
    pageProps: {
      // Call page-level getInitialProps
      ...(Component.getInitialProps
        ? await Component.getInitialProps(ctx)
        : {}),
      // Some custom thing for all pages
      appProp: ctx.pathname
    }
  };
};

export default wrapper.withRedux(App);

Upvotes: 4

Related Questions