How to implement redux-toolkit and next,js and not lose SSR - redux

I'm trying to implement redux-toolkit in my Next.js project without losing the option of SSR for the data I'm fetching from an external API. I have followed the example in next.js GitHub but doing so led to not having SSR when fetching data in my redux slice. I would like to know how I can fetch data and save that data in the Redux state. this is what I've written:
this is the store.js file
export const store = configureStore({
reducer: {
users: usersReducer,
},
});
the _app.js file
import { Provider } from 'react-redux';
import { store } from '../app/store';
const MyApp = ({ Component, pageProps }) => {
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
};
export default MyApp;
the usersSlice.js file
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
const initialState = {
items: null,
status: 'idle',
error: null,
};
export const fetchUsers = createAsyncThunk('users/fetchUsers', async () => {
const res = await fetch('https://jsonplaceholder.typicode.com/users');
const users = await res.json();
return users;
});
const usersSlice = createSlice({
name: 'categories',
initialState,
reducers: {},
extraReducers: {
[fetchUsers.pending]: (state, action) => {
state.status = 'loading';
},
[fetchUsers.fulfilled]: (state, action) => {
state.status = 'succeeded';
state.items = action.payload;
},
[fetchUsers.rejected]: (state, action) => {
state.status = 'failed';
state.error = action.error.message;
},
},
});
export default usersSlice.reducer;
and finally where the page I'm fetching the data from:
export default function Home() {
const dispatch = useDispatch();
const users = useSelector((state) => state.users.items);
useEffect(() => {
dispatch(fetchUsers());
}, [dispatch]);
return (
<div>
<h1>users</h1>
{users &&
users.length &&
users.map((user) => <p key={user.id}>{user.name}</p>)}
</div>
);
}
If I fetch data through dispatching the fetchUsers function it won't have SSR, and if I use getServerSideProps it won't be saved in redux state. I'm clueless

If you are okay to use Redux instead of redux-toolkit then follow this example
[https://github.com/vercel/next.js/blob/canary/examples/with-redux-persist]Officail Example form Vercel/next.js. I too facing some issue when i am writing SSR code for Redux persist with redux toolkit. The work is currently in progress. Will share the code when it is available. Sis

Related

How to use enhance store within redux middleware?

I am building a React-Redux application and need a middleware function that has access to an enhanced store. I am unable to get the enhanced store to be available in the middleware function. Is this possible, if so how?
https://codesandbox.io/s/redux-enhanced-store-in-middleware-e1c5uv?file=/src/main.ts
import {createElement} from 'react'
import {Provider, useDispatch} from 'react-redux'
import {configureStore, getDefaultMiddleware} from '#reduxjs/toolkit'
import { createRoot } from 'react-dom/client'
function reducer(state, action){
console.debug("reducer...")
return state
}
const apiMiddleware = (store) => (next) => (action) => {
console.debug("apiMiddleware", store) // I would like store.api here
return next(action)
}
const storeEnhancer = (next) => {
const api = {doSomething: () => console.debug("api.doSomething")}
return (reducer, initialState) => {
const the_store = {api, ...next(reducer, initialState)}
console.debug("storeEnhancer", the_store)
return the_store
}
}
const store: any = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware().concat(apiMiddleware),
enhancers: (defaultEnhancers) => [...defaultEnhancers, storeEnhancer],
})
const ClickButton = () => {
const dispatch = useDispatch()
const onClick = () => dispatch({type: "action"})
return createElement("button", {onClick}, "clicky")
}
export const app = () =>
{
const rootElement = document.getElementById("root")
const root = createRoot(rootElement!)
root.render(createElement(Provider, {store, children: createElement(ClickButton)}))
return createElement("div", {}, "hello")
}
Middleware don't get the entire Redux store as their outermost argument. Instead, they get a partial version - just {dispatch, getState}.
This is why I prefer to refer to that variable as storeApi, rather than store, because it isn't the entire store:
https://redux.js.org/tutorials/fundamentals/part-4-store#writing-custom-middleware
So yeah, if your enhancer is attaching extra fields to the store instance, you can't access those in the middleware.

React-redux Toolkit: Cannot set new state, when passing reducer as prop to another function

I am trying to use react redux toolkit and pass setter function to set new state on firebase's 'onAuthStateChanged'. The plan was to pass user's state (object or null) to reducer, depending if user is logged in or logged out. This is my first usage of redux, so I can't get why my code doesn't work. There is no errors, but in redux devtools state is always equal to null.
Configure Store:
import { configureStore } from '#reduxjs/toolkit'
import { Provider } from 'react-redux';
import userReducer from './utils/userReducer';
const store = configureStore({
reducer: {
user: userReducer,
}
})
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
<React.StrictMode>
<Provider store={store}>
<App />
</Provider>
</React.StrictMode>
);
My reducer:
import { createSlice } from "#reduxjs/toolkit";
export const userSlice = createSlice({
name: 'user',
initialState: null,
reducers: {
setUser: (state, action) => {
state = action.payload;
}
}
})
export const {setUser} = userSlice.actions;
export default userSlice.reducer;
Where I am dispatching it:
import { setUser } from '../utils/userReducer'
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
const handleLogin = async (e) => {
e.preventDefault()
const { user } = await logInWithEmail(email, password)
await setCurrentUser(() => dispatch(setUser))
}
Firebase function, where I am trying to use reducer:
export const setCurrentUser = async (setUser) => {
await onAuthStateChanged(auth, (currentUser) => {
setUser(currentUser)
})
}
I understand, that with useContext it would be much easier, but I am trying to learn redux by implying it.
Try like that:
import { setUser } from '../utils/userReducer'
import { useDispatch } from 'react-redux'
const dispatch = useDispatch()
const handleLogin = async (e) => {
e.preventDefault()
const { user } = await logInWithEmail(email, password)
// This line updated
await setCurrentUser((currentUser) => dispatch(setUser(currentUser)))
}
The reason:
your setCurrentUser function prop setUser is just function () => dispatch(setUser), but this function does not receive any prop, and dispatch(setUser) does not do anything. you need to pass value (payload) to reducer function.
Additionally, try passing dispatch itself as prop and dispatch inside of onAuthStateChanged.
export const setCurrentUser = async (dispatch) => {
await onAuthStateChanged(auth, (currentUser) => {
dispatch(setUser(currentUser))
})
}
import setUser reducer function if handleLogin and setCurrentUser function is in different files separately.

Redux actions to reducers not showing in devtools state

I'd managed to get some of my earlier functions state in devtools as below:
Reducers function in DevTools
But when I tried to query some of the events in my interactions, the functions state werent able to display it. Below are my codes and settings, basically the flow is interactions > actions > reducers
interaction code:
export const loadAllOrders = async (exchange, dispatch) => {
// Fetch cancelled orders with the "Cancel" event stream
const fromBlock = 0;
const toBlock = "latest";
const cancelFilter = exchange.filters.CancelOrder();
const cancelStream = await exchange.queryFilter(cancelFilter, fromBlock, toBlock);
console.log(cancelStream)
// Format cancelled orders
const cancelledOrders = cancelStream.map((event) => event.args);
// Add cancelled orders to the redux store
dispatch(cancelledOrdersLoaded(cancelledOrders));
}
from my actions:
export const cancelledOrdersLoaded = (cancelledOrders) => {
return {
type: 'CANCELLED_ORDERS_LOADED',
payload:cancelledOrders
}
}
from my reducers:
export const exchange = (state = initialState, action) => {
switch (action.type) {
case 'EXCHANGE_LOADED':
return { ...state, loaded:true, contract: action.payload }
case 'CANCELLED_ORDERS_LOADED':
return { ...state, cancelledOrders: action.payload }
default:
return state
}
my configureStore
// For redux dev tools
const devTools = window.__REDUX_DEVTOOLS_EXTENSION__ && window.__REDUX_DEVTOOLS_EXTENSION__()
const store = createStore(
rootReducer,
compose(applyMiddleware(thunk),devTools)
)
Thanks in advance
I haven't worked with redux for quite some time now, but from a quick look at some of my older repos, it seems like you didn't set up your store correctly.
This is what I have there,
import { applyMiddleware, createStore, compose, combineReducers } from "redux"
import thunk from "redux-thunk"
const composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose
const rootReducer = combineReducers({
reducers...
})
export const store = createStore(rootReducer, composeEnhancers(applyMiddleware(thunk)))

Error: Invariant failed: A state mutation was detected between dispatches, in the path 'emailReducer.emails.0'

I'm getting this error message about mutating state, but the purpose of Redux Toolkit is mutating state, so I'm confused...
The error is coming from addNewEmail, where I'm adding new emails to the array calling prevEmails using useSelector and the second parameter is a regular string.
import { createSlice } from "#reduxjs/toolkit";
import { AppThunk } from "./store";
const initialState = {
emails: [],
};
export const emailSlice = createSlice({
name: "email",
initialState,
reducers: {
setEmails: (state, action: any) => {
state.emails = action.payload;
},
},
});
export const { setEmails } = emailSlice.actions;
export const addNewEmail = (prevEmails: any, email: string): AppThunk => (
dispatch
) => {
const allEmails = prevEmails.push(email);
dispatch(setEmails(allEmails));
};
export default emailSlice.reducer;
export const selectEmails = (state: any) => state.emailReducer.emails;
I was also getting the same error, this is not how you disptach the
action. All you have to pass this middlewares in your store.
const store = configureStore({
reducer,
middleware: (getDefaultMiddleware) => getDefaultMiddleware({
immutableCheck: false,
serializableCheck: false,
})
})
As #asaf-aviv said, the real problem is that you're attempting to mutate what is actually state.emails, outside of a reducer:
const allEmails = prevEmails.push(email);
dispatch(setEmails(allEmails));
The second problem is conceptual. You should model actions as "events", not "setters", and put as much logic as possible into reducers. If you follow those guidelines, this problem won't occur in the first place.
Also, this doesn't even need to be a thunk - just dispatch an action that contains the new email object.
The right way to handle this is:
export const emailSlice = createSlice({
name: "email",
initialState,
reducers: {
emailAdded: (state, action: PayloadAction<Email>) => {
state.emails.push(action.payload)
},
},
});
export const { emailAdded } = emailSlice.actions;
// later
dispatch(emailAdded(newEmail));
You are mutating the state before dispatching the action, you can do mutations inside the reducer but not outside of it.
You can change prevEmails.push(email) to prevEmails.concat(email) which will return a new array which you can then send as a payload.

React-Redux: Error: Actions must be plain objects. Use custom middleware for async actions

I've read multiple sources about this error but I cannot figure out what I'm doing incorrectly here. I'm using custom middleware already and I believe that I'm returning the action correctly. Any advice?
app.js
import React from "react";
import ReactDOM from "react-dom";
import { renderToString } from "react-dom/server";
import { Provider } from "react-redux";
import { createStore, applyMiddleware, compose } from "redux";
import thunk from 'redux-thunk';
import rootReducer from '../reducers';
import DataProvider from "./DataProvider";
import QuestionContainer from "./QuestionContainer";
import * as actions from "../actions";
const App = () => <QuestionContainer />;
const store = createStore(
rootReducer,
applyMiddleware(thunk)),
);
store
.dispatch(actions.fetchQuestions())
.then(() => response.send(renderToString(<Provider store={ store }><App /></Provider>)))
Then in actions.js
export function fetchQuestions() {
return (dispatch) => {
return fetch('/api/questions')
.then(response => response.json())
.then(data => dispatch(loadRequestData(data)),
)
}
}
The error showing in browser console:
redux.js:208 Uncaught (in promise) Error: Actions must be plain objects. Use custom middleware for async actions.
at dispatch (redux.js:208)
at eval (index.js:12)
at dispatch (redux.js:571)
at eval (actions.js:35)
I think there's something's wrong in this part of code:
store
.dispatch(actions.fetchQuestions())
.then(() => response.send(renderToString(<Provider store={ store }><App /></Provider>)))
When you're creating async calls you want to do this only in action, not reducer/store.
So you need to delete this line .then(() => response.send(renderToString(<Provider store={ store }><App />
and instead of that just make:
const app = (
<Provider store={store}>
<App />
</Provider>
)
ReactDOM.render(app, document.getElementById('root'));
Additionally make some actions which will be kind of helper for updating store in your reducer. Something like this:
export const fetchBegin = () => ({
type: 'FETCH_BEGIN'
})
export const fetchSuccess = (payload) => ({
type: 'FETCH_SUCCESS'
payload
})
export const fetchQuestions = () => {
return (dispatch) => {
dispatch(fetchBegin())
return fetch('/api/questions')
.then(response => response.json())
.then(data => dispatch(fetchSuccess(data))
)
}
}
Then in the reducers make:
const initialState = {
call: [],
loading: false
}
const reducer = (state = initialState, action){
switch(action.type){
case 'FETCH_BEGIN:
return{
...state,
loading: true,
case 'FETCH_SUCCESS':
return{
...state,
call: action.payload,
loading: false,
}
default:
return state
}
}
This should work imho.

Resources