How to share redux state client-side and props server-side in Next JS - reactjs

I'm a newbie with Next JS.
I use Next JS and Redux.
I have a short code below:
const AdminContainer = (props) => {
return (
<AdminMasterView>
<DashboardView studentList={props.studentListServer}/>
</AdminMasterView>
)
}
export const getStaticProps = (async () => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList
});
const mapDispatchToProps = {
getStudentRegisterAction
};
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer);
I also have studentList (array type) props is declare in Redux. I want to use it to pass data because I have many tasks to do with data such as filter, order,...
Is there any way to use studentList like this and my app still is server rendering first time.
If I dispatch studentListServer to studentList, it still work. But my app isn't server rendering.
<DashboardView studentList={props.studentList}/>
Or easier, I'll check to use props.studentList for client-side and props.studentListServer for server-side. But I think it's not good.
Thank you so much!

You could use the next-redux-wrapper package. It allows to sync a Redux state on server and client. Consider the example:
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
// dispatch the action that saves the data
store.dispatch({ type: 'SET_STUDENTS', payload: response });
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
wrapper.getStaticProps wraps your getStaticProps function with the new parameter store that is a Redux store in fact.
Action with type SET_STUDENTS sets the student list on a server side. When Next.js generates the page, it will save this data in static JSON. So when the page opens on client side, next-redux-wrapper recreates a state dispatching HYDRATE action with saved on a build time static JSON that you can use to restore the studentInfoReducers reducer.
E.g. in your reducer you should implement something like:
import { HYDRATE } from 'next-redux-wrapper';
const initialState = { studentList: [] };
// studentInfoReducers reducer
function reducer(state = initialState, action) {
// this sets your student list
if (action.type === 'SET_STUDENTS') {
return {
...state,
studentList: action.payload,
};
}
// this rehydrates your store from server on a client
if (action.type === HYDRATE) {
return action.payload.studentInfoReducers;
}
return state;
}
So afterwards you should have a valid synced state on client and server at the same time:
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList // works on server and client
});
Let me know if you have any questions, next-redux-wrapper can be tricky from a first look.

You don't need to use Redux for that.
Using just cookies you can achieve bidirectional communication, see https://maxschmitt.me/posts/next-js-cookies/
Another example:
Client to Server: manually set a cookie in the client side and then read it in the server with req.headers.cookie or some library like 'cookie'
Server to Client: just read the cookie, and return what you need as a regular prop or update the cookie.
import { useState, useEffect } from "react";
import Cookie from "js-cookie";
import { parseCookies } from "../lib/parseCookies";
const Index = ({ initialRememberValue = true }) => {
const [rememberMe, setRememberMe] = useState(() =>
JSON.parse(initialRememberValue)
);
useEffect(() => {
//save/create the cookie with the value in the client
Cookie.set("rememberMe", JSON.stringify(rememberMe));
}, [rememberMe]);
return (
<div>
remember me
<input
type="checkbox"
value={rememberMe}
checked={rememberMe}
onChange={e => setRememberMe(e.target.checked)}
/>
</div>
);
};
Index.getInitialProps = ({ req }) => {
//read the cookie on the server
const cookies = parseCookies(req); //parseCookies is a simple custom function you can find
return {
//send the value as a regular prop
initialRememberValue: cookies.rememberMe
};
};
export default Index;
Reference: https://github.com/benawad/nextjs-persist-state-with-cookie/blob/master/pages/index.js

Related

React Testing Library - how to correctly test that my provider is being populated with data and the child components are displaying properly?

I have a pretty simple use case - I have a global app context where I'm trying to store data fetched from an endpoint. My goal is to load this data into the context on app load and I'm going about it using the useReducer hook. I settled on the solution of calling an action getIssuerDetails() that dispatches various states throughout the method and invokes the issuerApi service to actually call the API (it's a simple Axios GET wrapper). This action is called from a useEffect within the Provider and is called on mount as shown below.
I'm having some trouble wrapping my head around how to properly test that 1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider. Am I approaching this data fetching portion correctly? I can either make the actual API call within my App component on mount and then dispatch actions to update the global state OR I keep my solution of fetching my data from within the useEffect of the provider.
I know I'm not supposed to be testing implementation details but I'm having a hard time separating out what data/methods I should mock and which ones I should allow to execute on their own. Any help would be greatly appreciated.
AppContext.tsx
import { createContext, FC, useEffect, useContext, useReducer, useRef } from 'react';
import { getIssuerDetails } from './issuer/actions';
import { appStateReducer } from './global/reducer';
import { combineReducers } from '#utils/utils';
import { GlobalAppStateType } from './global/types';
/**
* Our initial global app state. It just stores a bunch
* of defaults before the data is populated.
*/
export const defaultInitialState = {
issuerDetails: {
loading: false,
error: null,
data: {
issuerId: -1,
issuerName: '',
ipoDate: '',
ticker: '',
},
},
};
export type AppStateContextProps = {
state: GlobalAppStateType;
};
export type AppDispatchContextProps = {
dispatch: React.Dispatch<any>;
};
export const AppStateContext = createContext<AppStateContextProps>({
state: defaultInitialState,
});
export const AppDispatchContext = createContext<AppDispatchContextProps>({
dispatch: () => null,
});
/**
*
* #param
* #returns
*/
export const mainReducer = combineReducers({
appState: appStateReducer,
});
export type AppProviderProps = {
mockInitialState?: GlobalAppStateType;
mockDispatch?: React.Dispatch<any>;
};
/**
* Our main application provider that wraps our whole app
* #param mockInitialState - mainly used when testing if we want to alter the data stored in our
* context initially
* #param children - The child components that will gain access to the app state and dispatch values
*/
export const AppProvider: FC<AppProviderProps> = ({ mockInitialState, mockDispatch, children }) => {
const [state, dispatch] = useReducer(mainReducer, mockInitialState ? mockInitialState : defaultInitialState);
const nState = mockInitialState ? mockInitialState : state;
const nDispatch = mockDispatch ? mockDispatch : dispatch;
// Ref that acts as a flag to aid in cleaning up our async data calls
const isCanceled = useRef(false);
useEffect(() => {
async function fetchData() {
// Await the API request to get issuer details
if (!isCanceled.current) {
await getIssuerDetails(nDispatch);
}
}
fetchData();
return () => {
isCanceled.current = true;
};
}, [nDispatch]);
return (
<AppStateContext.Provider value={{ state: nState }}>
<AppDispatchContext.Provider value={{ dispatch: nDispatch }}>{children}</AppDispatchContext.Provider>
</AppStateContext.Provider>
);
};
/**
* Custom hook that gives us access to the global
* app state
*/
export const useAppState = () => {
const appStateContext = useContext(AppStateContext);
if (appStateContext === undefined) {
throw new Error('useAppState must be used within a AppProvider');
}
return appStateContext;
};
/**
* Custom hook that gives us access to the global
* app dispatch method to be able to update our global state
*/
export const useAppDispatch = () => {
const appDispatchContext = useContext(AppDispatchContext);
if (appDispatchContext === undefined) {
throw new Error('useAppDispatch must be used within a AppProvider');
}
return appDispatchContext;
};
AppReducer.ts
Note: Code still needs to be cleaned up here but it's functioning at the moment.
import * as T from '#context/global/types';
export const appStateReducer = (state: T.GlobalAppStateType, action: T.GLOBAL_ACTION_TYPES) => {
let stateCopy;
switch (action.type) {
case T.REQUEST_ISSUER_DETAILS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = true;
return stateCopy;
case T.GET_ISSUER_DETAILS_SUCCESS:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.data = action.payload;
return stateCopy;
case T.GET_ISSUER_DETAILS_FAILURE:
stateCopy = { ...state };
stateCopy.issuerDetails.loading = false;
stateCopy.issuerDetails.error = action.payload;
return stateCopy;
default:
return state;
}
};
getIssuerDetails()
export const getIssuerDetails = async (dispatch: React.Dispatch<any>) => {
dispatch({ type: GlobalState.REQUEST_ISSUER_DETAILS, payload: null });
try {
// Fetch the issuer details
const response = await issuerApi.getIssuerDetails(TEST_ISSUER_ID);
if (response) {
/***************************************************************
* React Testing Library gives me an error on the line below:
* An update to AppProvider inside a test was not wrapped in act(...)
***************************************************************/
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_SUCCESS, payload: response });
return response;
}
// No response
dispatch({
type: GlobalState.GET_ISSUER_DETAILS_FAILURE,
error: { message: 'Could not fetch issuer details.' },
});
} catch (error) {
dispatch({ type: GlobalState.GET_ISSUER_DETAILS_FAILURE, error });
}
};
dashboard.test.tsx
import { render, screen, cleanup, act } from '#testing-library/react';
import { AppProvider, AppStateContext } from '#context/appContext';
import { GlobalAppStateType } from '#context/global/types';
afterEach(() => {
cleanup();
jest.clearAllMocks();
});
describe('Dashboard page', () => {
it('should render the page correctly', async () => {
act(() => {
render(
<AppProvider>
<Dashboard />
</AppProvider>
);
});
expect(await screen.findByRole('heading', { level: 1 })).toHaveTextContent('Stock Transfer');
});
});
I won't dive into the code specifically since there is too much you want to test all at once.
From what I could gather, you are trying to do an Integration Test and not a Unitary Test anymore. No problem there, you just need to define where you want to draw the line. For me, it's pretty clear that the line lies in the issuerApi.getIssuerDetails call, from which you could easily mock to manipulate the data how you want.
1) my AppProvider actually gets populated with the data fetched within the useEffect and 2) my child components within my AppProvider are being populated correctly with the data passed down from the provider.
Well, I would advise you to make a simple mock component that uses the hook and displays the data after fetching. You could make a simple assertion for that, no need for an actual component (<Dashboard />).
Am I approaching this data fetching portion correctly?
It all depends on how you want to structure it but ideally the AppProvider should be thin and lay those data fetching and treatments inside a service just for that. This would provide a better way to unit test the components and smoother code maintenance.

Update single element of the array in redux state

I have a redux state that contains an array of objects, for each of these object I call an api to get more data
objects.forEach((obj, index) => {
let newObj = { ...obj };
service.getMoreData()
.then(result => {
newObj.data = result;
let newObjects = [...this.props.objectsList] ;
let index = newObjects.findIndex(el => el.id === newObj.id);
if (index != -1) {
newObjects[index] = newObj;
this.props.updateMyState({ objectsList: newObjects });
}
})
When I get two very close responses the state is not updated correctly, I lose the data of the first response.
What is the right way to update a single element of the array? Thanks!
So since i don't know what service is and there isn't that much here to go off, here is what I would do from my understanding of what it looks like your doing:
So first let's set up a reducer to handle the part of redux state that you want to modify:
// going to give the reducer a default state
// array just because I don't know
// the full use case
// you have an id in your example so this is the best I can do :(
const defaultState = [{ id: 123456 }, { id: 123457 }];
const someReducer = (state=defaultState, action) => {
switch (action.type) {
// this is the main thing we're gonna use
case 'UPDATE_REDUX_ARRAY':
return [
...action.data
]
// return a default state == the state argument
default:
return [
...state
]
}
}
export default someReducer;
Next you should set up some actions for the reducer, this is optional and you can do it all inline in your component but I'd personally do it this way:
// pass data to the reducer using an action
const updateReduxArray = data => {
return {
type: 'UPDATE_REDUX_ARRAY',
data: data
}
}
// export like this because there might
// be more actions to add later
export {
updateReduxArray
}
Then use the reducer and action with React to update / render or whatever else you want
import { useState } from 'react';
import { updateReduxArray } from 'path_to_actions_file';
import { useEffect } from 'react';
import { axios } from 'axios';
import { useSelector, useDispatch } from 'react-redux';
const SomeComponent = () => {
// set up redux dispatch
const dispatch = useDispatch();
// get your redux state
const reduxArray = useSelector(state => state.reduxArray) // your gonna have to name this however your's is named
// somewhere to store your objects (state)
const [arrayOfObjects, updateArrayOfObjects] = useState([]);
// function to get data from your API
const getData = async () => {
// I'm using axios for HTTP requests as its pretty
// easy to use
// if you use map you can just return the value of all API calls at once
const updatedData = await Promise.all(reduxArray.map(async (object, index) => {
// make the api call
const response = axios.get(`https://some_api_endpoint/${object.id}`)
.then(r => r.data)
// return the original object with the addition of the new data
return {
...response,
...object
}
}))
// once all API calls are done update the state
// you could just update redux here but this is
// a clean way of doing it incase you wanna update
// the redux state more than once
// costs more memory to do this though
updateArrayOfObjects(updatedData)
}
// basicity the same as component did mount
// if you're using classes
useEffect(() => {
// get some data from the api
getData()
}, [ ])
// every time arrayOfObjects is updated
// also update redux
useEffect(() => {
// dispatch your action to the reducer
dispatch(updateReduxArray(arrayOfObjects))
}, [arrayOfObjects])
// render something to the page??
return (
<div>
{ reduxArray.length > 0
? reduxArray.map(object => <p>I am { object.id }</p>)
: <p>nothing to see here</p>
}
</div>
)
}
export default SomeComponent;
You could also do this so that you only update one object in redux at a time but even then you'd still be better off just passing the whole array to redux so I'd do the math on the component side rather than the reducer .
Note that in the component I used react state and useEffect. You might not need to do this, you could just handle it all in one place when the component mounts but we're using React so I just showcased it incase you want to use it somewhere else :)
Also lastly I'm using react-redux here so if you don't have that set up (you should do) please go away and do that first, adding your Provider to the root component. There are plenty of guides on this.

In my React app, why isn't this Redux reducer executed?

Good afternoon,
After doing the React tutorial, and read all the React guides, I did the Redux tutorial to re-write my working auth component to use Redux instead of component state (I also switched from classes to functional components).
My Redux setup is as follow :
import { createSlice } from '#reduxjs/toolkit';
const sessionInitialState = {
authToken: null,
userLogin: null,
};
export const sessionSlice = createSlice({
name: 'session',
initialState: {
value: sessionInitialState,
},
reducers: {
sessionRegister: (state, action) => {
console.log("Storing:")
console.log(action.payload)
state.value = action.payload;
},
sessionDestroy: state => {
console.log("Destroying session")
state.value = sessionInitialState;
localStorage.removeItem("sessionData");
},
},
});
export const { sessionRegister, sessionDestroy } = sessionSlice.actions;
export const selectSession = state => state.session.value;
export default sessionSlice.reducer;
In my login component, I store the auth token and the username as follow :
// [...]
import { useDispatch } from 'react-redux';
import { sessionRegister } from './SessionSlice';
// [...]
function LoginForm(props) {
const dispatch = useDispatch();
function onFormSubmit(event) {
// [...] This is when API response code is 200:
let sessionData = {
authToken: response.data.token,
userLogin: userName,
}
localStorage.setItem("sessionData", JSON.stringify(sessionData));
console.log("Stored:" + localStorage.getItem("sessionData"));
setApiError(null);
dispatch(sessionRegister(sessionData));
And my App component displays the login form if no data is stored in Redux, or the user name and token if they're present :
function App(props) {
const sessionData = useSelector(selectSession);
const storedSessionData = localStorage.getItem("sessionData");
if (storedSessionData && !sessionData.authToken) {
console.log("App init check. Storing :");
console.log(JSON.parse(storedSessionData));
sessionRegister(JSON.parse(storedSessionData));
} else {
if (!storedSessionData) console.log("No sessionData and no stored data");
}
return (
<div className="App">
<div className="topbar"><Topbar /></div>
<div className="content">
{sessionData.authToken ? (
<span>User: {sessionData.userLogin} {storedSessionData}</span>
) : (
<LoginForm />
)
}
</div>
</div>
);
}
All this almost works. The login component stores the data via Redux after the API call, and in localStorage too. The userLogin from Redux is displayed by the App, and the localStorage content too. But if I refresh the page, the App gets the data from locaStorage but sessionRegister is not called (or does nothing).
It happens as follow :
Fisrt open of the page. Console: No sessionData and no stored data
Login via LoginForm. Console:
Localy stored:{"authToken":"5467c25e000df49a1161c4ff8ga1f610053f62b8","userLogin":"testuser"}
Storing in Redux:
Object { authToken: "5467c25e000df49a1161c4ff8ga1f610053f62b8", userLogin: "testuser" }
Now the App component correctly renders the content instead of the login form :
User: testuser {"authToken":"5467c25e000df49a1161c4ff8ga1f610053f62b8","userLogin":"testuser"}
So far, so good. But if I refresh the page, the login form is displayed again. In the console I get :
App init check. Storing : App.js:15
Object { authToken: "5467c25e000df49a1161c4ff8ga1f610053f62b8", userLogin: "testuser" } App.js:16
App init check. Storing : App.js:15
Object { authToken: "5467c25e000df49a1161c4ff8ga1f610053f62b8", userLogin: "testuser" } App.js:16
I don't understand why sessionRegister(JSON.parse(storedSessionData)); is not correctly executed, and why do I get twice the console log App init check.
Thanks for having read all this, any help would be appreciated.
I think you're missing a dispatch
Instead of
sessionRegister(JSON.parse(storedSessionData));
it should be
dispatch(sessionRegister(JSON.parse(storedSessionData)));
in your App component. You get access to dispatch with a hook
const dispatch = useDispatch()
last, but not least, you're updating the store in every render, so that's why you see the log twice. I think you should probably move this to an Effect
const dispatch = useDispatch()
const sessionData = useSelector(selectSession);
useEffect(() => {
const storedSessionData = localStorage.getItem("sessionData");
if (storedSessionData && !sessionData.authToken) {
console.log("App init check. Storing :");
console.log(JSON.parse(storedSessionData));
sessionRegister(JSON.parse(storedSessionData));
} else {
if (!storedSessionData) console.log("No sessionData and no stored data");
}
}, [dispatch, sessionData])

Simple way to store React state between components

I have a React app that has multiple tabs. When the user goes to the "Data" tab it will fetch data from an API call and set the data in a React state. However, if the user goes from "Data" tab to "Home" tab then back to "Data" tab it will have to fetch the data again from API call because data in state has disappeared.
Psuedocode of desired functionality:
const OutputTab: React.FC<PageProps> = ({ match, pageName }) => {
const [outputData, setOutputData] = useState<outputsInterface[]>([]);
useIonViewWillEnter(() => {
if (!outputData) {
fetchOutputs();
}
});
const fetchOutputs = () => {
let response = fetch("....");
setOutputData(response.json);
};
};
What is the simplest way to store the state data? Desired functionality is when user comes back to the tab we can simply check if data already exists rather than making another API call to refetch.
I thought of possible solutions to use localStorage or sessionStorage but I'd prefer to store the data in memory rather than storage. Do I need something like Redux to accomplish this?
full solution with video examples here with source code in codesandbox
https://youtu.be/DiCzp5kIcP4
https://dev.to/aaronksaunders/two-ways-to-manage-state-in-react-js-5dkb
Using Context API
import React from "react";
// create the context
export const Context = React.createContext();
// create the context provider, we are using use state to ensure that
// we get reactive values from the context...
export const TheProvider = ({ children }) => {
// the reactive values
const [sharedValue, setSharedValue] = React.useState({
value: "initial",
changedBy: "Admin"
});
// the store object
let state = {
sharedValue,
setSharedValue
};
// wrap the application in the provider with the initialized context
return <Context.Provider value={state}>{children}</Context.Provider>;
};
export default Context;
Using Reducer with useReducer
const reducer = (state: IState, action: ActionType): IState => {
switch (action.type) {
case "update":
return { ...state, ...action.payload };
case "clear":
return { ...state, ...action.payload, value: "" };
default:
throw new Error();
}
};
const [state, dispatch] = React.useReducer(reducer, initialState);

Best Way to load model into redux store on route load

I have a React app that uses React-Router/React-Router-dom for page navigation and redux to store some global state info (jwt token for django rest framework for example). The state also stores info about the currently viewed page, such as the serialized django model.
But what is the best way to load the django model into the redux store when the route changes? I'm having trouble wrapping my head around where logic should be going.
If you view the repo below you can see where I'm having trouble figuring it out.
In this example when someone navigates to /spells/:id, it should load the spell django model into the redux store so information about it is globally accessible.
But how do I go about doing that? Where do I call the actions and reducers to properly handle the state?
Any guidance would be appreciated.
You can view the full project here. The component in question here is LayoutSpellView (/frontend/src/components/LayoutSpellView). That's where the model information is stored, displayed, etc.
Edit: Adding relevant code
Called in componentDidMount:
axios
.get("http://localhost:3000/api/spells/" + spellId)
.then(response => {
let spell = Object.assign({}, spellView.state.spell);
spell.id = response.data.id;
spell.owner = response.data.owner;
...blahblah other fields
this.setState({
spell
});
})
.then(response => {
this.props.dispatch({
type: 'FETCH_SPELL_SUCCESS',
payload: this.state.spell,
});
})
.catch(function(error) {
console.error('[API]\t', error);
});
In LayoutSpellView (same component as above)
import {loadSpell} from "../src/reducers";
const mapStateToProps = (state) => ({
spell: loadSpell(state.spell.id),
});
const mapDispatchToProps = (dispatch) => ({
getSpell: (state.spell.id) => {
dispatch(loadSpell(state.spell.id))
}
});
Actions spell.js:
export const FETCH_SPELL = '##spell/FETCH_SPELL';
export const FETCH_SPELL_SUCCESS = '##spell/FETCH_SPELL_SUCCESS';
export const FETCH_SPELL_FAILURE = '##spell/FETCH_SPELL_FAILURE';
export const loadSpell = (spellId) => ({
[RSAA]: {
endpoint: '/api/spell/${spellId}',
method: 'GET',
types: [
FETCH_SPELL, FETCH_SPELL_SUCCESS, FETCH_SPELL_FAILURE
]
}
});
Reducers spell.js:
const initialState = {
spell: {
id: 0,
owner: 0,
Name: 'Name',
School: 'unknown',
Subschool: 'unknown',
}
};
export default (state=initialState, action) => {
switch(action.type) {
case spell_action.FETCH_SPELL_SUCCESS:
return {
spell: {
id: action.payload.spell.id,
owner: action.payload.spell.owner,
Name: action.payload.spell.Name,
School: action.payload.spell.School,
Subschool: action.payload.spell.Subschool,
}
};
default:
return state;
}
}
export function loadSpell(state) {
if (state) {
return state.spell
}
}
Let's look at the question in a different way. Instead of asking "How do I dispatch an action when routes change", let's ask "What is the actual source of truth: Redux or URL?"
If we go with redux being the Single Source of Truth, then that would mean that we need to dispatch some action that would cause some side-effect ( maybe redux-saga or redux-observable or even redux-thunk? ) that changed the url:
Comp -> dispatch(action) -> store updates -> URL changes
If we go with the URL being the Single Source of Truth, we change the flow to:
URL changes -> dispatch(action) -> store updates
If we go this route, which is what it sounds like you are wanting, you will need to probably hook up middleware, which are functions of the following signature:
store => next => action => next(action)
Depending on the router that you are using, you can either hook into their actions or you can hook into window.onpopstate and check the next url. Either way, the overall middleware function would look something like
const middleware = store => {
return next => action => {
if (actionWillCauseSpellToBeNeeded(action)) {
makeAPICall()
.then(transformAPIToAction)
.catch(transformError)
.then(store.dispatch)
}
return next(action)
}
}

Resources