How to use getStaticProps in combination with useReducer and useContext - reactjs

I am converting a React app to a NextJS app and cannot figure out how to use the props of getStaticProps into the initial state of my reducer inside my context.
In my current setup, my AppContext looks like this:
import { createContext, useReducer } from "react";
import { initialState, appReducer } from "../reducers/appReducer";
const AppContext = createContext();
const AppContextProvider = ({ children }) => {
const [state, dispatch] = useReducer(appReducer, initialState);
return (
<AppContext.Provider value={[state, dispatch]}>
{children}
</AppContext.Provider>
);
};
export { AppContext, AppContextProvider };
The I have my appReducer as follows:
export const initialState = {
example: null,
anotherExample: []
};
export const AppReducer = (state, action) => {
const { type, payload } = action;
switch (type) {
case "example_action":
return {...state, example: payload.example }
}
default:
return state;
}
What I do not understand is how I can get the props of getStaticProps in the initial state of my reducer (since my whole application is based on this context with reducer).
ExamplePage (pages/example-page.js)
export default function ExamplePage({ data }) {
const [state, dispatch] = useReducer(appReducer, {
...initialState,
example: data.example
});
return (
<AppContext.Provider value={[state, dispatch]}>
<div>...</div> // Rest of my application
<AppContext.Provider>
);
}
export export async function getStaticProps() {
const { data } = // Fetching my data ....
return {
props: {
data: data,
},
};
}
What I tried to do is move the reducer out of the AppContext, so I can pass the props from getStaticProps to it, but then I cannot use the AppContext including my reducer anymore in other places in my application. Or should I create my context like this:
AppContext.js
import { createContext } from "react";
const AppContext = createContext();
// Don't create my provider here, do it in ExamplePage?
export default AppContext;
And import this everywhere I want to use this context and create the provider inside my ExamplePage only?
I have the feeling I do not understand the concept of context, reducer with NextJS, is that true?

I didn't fully understand what exactly you want to achieve, but I think you're using contexts in the wrong place.
If, as I understand it, your whole React application was wrapped in a Context Provider, and you want to repeat this in nextjs, the _app.js file is best suited for this. This is a special file in nextjs, more about it here: https://nextjs.org/docs/advanced-features/custom-app, if you don't have it, you need to create it in the pages folder.
In your case it will be something like this:
// ./pages/_app.js
const AppContext = createContext();
export default function MyApp({ Component, pageProps }) {
const [state, dispatch] = useReducer(appReducer, initialState);
return(
<AppContext.Provider value={[state, dispatch]}>
<Component {...pageProps} />
</AppContext.Provider>
)
}
The _app.js is a special file in nextjs, all pages of your site will be wrapped in it, you can use this for your global states, they will not be reset when the user navigates through the pages of the site.
getStaticProps is designed for a completely different purpose, getStaticProps is designed to process data for a specific page, and not for all pages of the site.

Related

I believe I have created a shareable state using useReducer only, I need advice if this is wrong

Store.js:
import {useReducer} from "react";
import {ACTION_TYPES} from "./storeActionTypes";
export const INITIAL_STATE = {
counter: 0,
};
export const storeReducer = (state, action) => {
switch (action.type) {
case ACTION_TYPES.INCREASE_COUNTER_BY_1:
return {
...state,
counter: state.counter + 1,
};
default:
return state;
}
};
const Store = () => {
const [state, dispatch] = useReducer(storeReducer, INITIAL_STATE);
return [state, dispatch];
};
export default Store;
AnyComponent.js
import React from "react";
import Store from "../store/Store";
const AnyComponent = () => {
const [store, dispatch] = Store();
const handleInceaseByOne = (e) => {
e.preventDefault();
dispatch({type: "INCREASE_COUNTER_BY_1"});
};
return (
<div>
<button onClick={(e) => handleInceaseByOne(e)}>Submit</button>
<span>counter from AnyComponent.js:{store.counter}</span>
</div>
);
};
export default AnyComponent;
OtherComponent.js
import React from "react";
import Store from "../store/Store";
const OtherComponent.js = () => {
const [store, dispatch] = Store();
return (
<div>
<span>counter from OtherComponent.js:{store.counter}</span>
</div>
);
};
export default OtherComponent.js;
So basically like in Redux, create a one Store where you store everything. In AnyComponent.js we have button who increase counter by 1 so we can see that value of store.counter in AnyComponent.js and OtherComponent.js.
Please anyone tell me if anything is wrong with this code?
Will try to upload this to GitHub later.
I looked in web and did not found anything what is similar to this so please let me know what you think.
If you actually try this one, you will see that the counter state of one component does not reflect on the second one. You haven't created a single state but 2 separate ones.
Since you call Store() in each component which leads to a new call to useReducer, you create a new instance of this reducer/state which is standalone and can only be used from the component where it's called (or it's children if passed down as props).
What you've done here is to create your own custom hook and this is used for re-usability only and not shared state. Shared state can be achieved through a lot of other alternatives (such as react context).
Feel free to check this out in this reproducable codesandbox.
From the React Docs:
"Only call Hooks from React function components. Don’t call Hooks from regular JavaScript functions. (There is just one other valid place to call Hooks — your own custom Hooks. [...]." (https://reactjs.org/docs/hooks-overview.html)

Implementing Redux functionality using Context and Reducers in React not working Properly

So i am trying to implement the functionality of redux using only react hooks as shown in the following links https://codeburst.io/global-state-with-react-hooks-and-context-api-87019cc4f2cf & https://www.sitepoint.com/replace-redux-react-hooks-context-api/ But I cant seem to get it to work properly for some reason. I am trying to implement a basic setup where the DO_ACTION action is dispatched when button is clicked and the counter global state is shown below the button. When I do click on the increment action button after clicking it a few times, I get undefined for some reason. What am I doing wrong here?
Before clicking
After clicking 3 or 4 times
Here is my folder structure just in case you think i am importing wrong stuff
Button component
import './Button.css';
import React, {useEffect, useContext} from 'react';
import {Context} from '../../store/Store.js';
const Button = ( props ) => {
const [state, dispatch] = useContext(Context);
const incrementAction = () => {
dispatch({
type:'DO_ACTION',
payload: 1
})
console.log(state.number);
};
return (
<div>
<button onClick={() => incrementAction()}>Display Number!!!!</button>
<div>{state.number}</div>
</div>
)
};
export default Button
here is my reducer
const Reducer = (state, action) => {
switch (action.type) {
case 'DO_ACTION':
return state + action.payload
}
};
export default Reducer;
Here is my global store and HOC
import React, {createContext, useReducer} from "react";
import Reducer from './Reducer';
const initialState = {
number: 5
};
const Store = ({children}) => {
const [state, dispatch] = useReducer(Reducer, initialState);
return (
<Context.Provider value={[state, dispatch]}>
{children}
</Context.Provider>
)
};
export const Context = createContext(initialState);
export default Store;
And here is my main App component that is wrapped with store to have it accessible throughout my app
import Store from './store/Store.js';
import Button from './components/Button/Button.js';
const App = () => {
return (
<div>
<Store>
<Button />
<OtherComponents /> //a bunch of other components go here like title, paging etc
</Store>
</div>
);
}
export default App;
You need to be returning the whole (cloned) state object from your reducer function, not just the property you want to update. You should also make sure you have a default case:
const Reducer = (state, action) => {
switch (action.type) {
case 'DO_ACTION':
return {
number: state.number + action.payload
}
default:
return state;
}
};
export default Reducer;
If you have more than one property in your state object you must remember to clone all of the other properties into it as well (e.g. {...state, number: state.number + action.payload}).

React Context value never updates

I have the following code as a component that returns a context. For some reason when I call the updateUser function it is not setting the state, I keep getting a blank object. Is there something different I have to do to a function like that when it has parameters?
import React, { useState } from "react";
const UserContext = React.createContext({
user: {},
updateUser: (incomingUser) => {},
});
const UserData = (props) => {
const [user, setUser] = useState({});
const updateUser = (incomingUser) => {
setUser(incomingUser);
console.log(`user=${JSON.stringify(incomingUser)}`);
};
return (
<UserContext.Provider value={{ user, updateUser }}>
{props.children}
</UserContext.Provider>
);
};
export { UserData, UserContext };
Get rid of the default value for UserContext, or initialise it as null:
const UserContext = React.createContext(null);
Per the React Docs:
When React renders a component that subscribes to this Context object it will read the current context value from the closest matching Provider above it in the tree.
The defaultValue argument is only used when a component does not have a matching Provider above it in the tree. This default value can be helpful for testing components in isolation without wrapping them.

Next.js app doesn't seem to preserve global context provider set up within _app.js

I have my next.js app set up with a global context/provider dealy:
_app.js:
import {useState} from "react"
import GlobalContext from "components/GlobalContext"
function MyApp({ Component, pageProps }) {
const [isLoggedIn, setIsLoggedIn] = useState(null)
let contextValues = {
isLoggedIn, setIsLoggedIn
}
return (
<GlobalContext.Provider value={contextValues}>
<Component {...pageProps} />
</GlobalContext.Provider>
)
}
export default MyApp
components/GlobalContext.js:
const { createContext } = require("react")
const GlobalContext = createContext()
export default GlobalContext
When I call setIsLoggedIn(true) elsewhere in the app... it appears to be set just fine, but then when I navigate to a different page, it seems this global state gets blasted away and isLoggedIn is set back to false.
Is this how it's supposed to work?
How should I manage global state such as this, and also have it persist across page loads?
Thanks.
As I understand it, every time you go to a different page, the function MyApp is run, so a new 'instance' of the component's state is created.
To reuse the state, it sould be stored in a global state. I did that using an external redux store, and calling it from myapp. This store is a global variable and it's reused if it exists. Kind of the 'singleton' pattern.
let store: Store<StoreState, Action>;
const initStore = () => {
if (!store) {
store = createStore(reducer, initialState);
}
return store;
};
export const useStore = () => initStore();
Them from MyApp
import { useStore } from '../store';
import { Provider } from 'react-redux';
export default class MyApp extends App {
render() {
const { Component, pageProps } = this.props;
const store = useStore();
return (
<Provider store={store}>
<Component {...pageProps} />
</Provider>
);
}
}
You can maybe do the same without redux. The key is to keep the state as global variable.
However, this will work only from client side navigation. If you retrieve a page using server side, you'll lose the state, you'll need to use sessions or cookies for that.
You can also check this example from nextjs:
https://github.com/vercel/next.js/tree/canary/examples/with-redux

I am using React Context and would like to get confirmed my structure

It is my first application using react context with hooks instead of react-redux and would like to get help of the structure of the application.
(I'm NOT using react-redux or redux-saga libraries.)
Context
const AppContext = createContext({
client,
user,
});
One of actions example
export const userActions = (state, dispatch) => {
function getUsers() {
dispatch({ type: types.GET_USERS });
axios
.get("api address")
.then(function(response) {
dispatch({ type: types.GOT_USERS, payload: response.data });
})
.catch(function(error) {
// handle error
});
}
return {
getUsers,
};
};
Reducer (index.js): I used combineReducer function code from the redux library
const AppReducer = combineReducers({
client: clientReducer,
user: userReducer,
});
Root.js
import React, { useContext, useReducer } from "react";
import AppContext from "./context";
import AppReducer from "./reducers";
import { clientActions } from "./actions/clientActions";
import { userActions } from "./actions/userActions";
import App from "./App";
const Root = () => {
const initialState = useContext(AppContext);
const [state, dispatch] = useReducer(AppReducer, initialState);
const clientDispatch = clientActions(state, dispatch);
const userDispatch = userActions(state, dispatch);
return (
<AppContext.Provider
value={{
clientState: state.client,
userState: state.user,
clientDispatch,
userDispatch,
}}
>
<App />
</AppContext.Provider>
);
};
export default Root;
So, whenever the component wants to access the context store or dispatch an action, this is how I do from the component :
import React, { useContext } from "react";
import ListMenu from "../common/ListMenu";
import List from "./List";
import AppContext from "../../context";
import Frame from "../common/Frame";
const Example = props => {
const { match, history } = props;
const { userState, userDispatch } = useContext(AppContext);
// Push to user detail route /user/userId
const selectUserList = userId => {
history.push(`/user/${userId}`);
userDispatch.clearTabValue(true);
};
return (
<Frame>
<ListMenu
dataList={userState.users}
selectDataList={selectUserList}
/>
<List />
</Frame>
);
};
export default Example;
The problem I faced now is that whenever I dispatch an action or try to access to the context store, the all components are re-rendered since the context provider is wrapping entire app.
I was wondering how to fix this entire re-rendering issue (if it is possible to still use my action/reducer folder structure).
Also, I'm fetching data from the action, but I would like to separate this from the action file as well like how we do on redux-saga structure. I was wondering if anybody know how to separate this without using redux/redux-saga.
Thanks and please let me know if you need any code/file to check.
I once had this re-rendering issue and I found this info on the official website:
https://reactjs.org/docs/context.html#caveats
May it will help you too
This effect (updating components on context update) is described in official documentation.
A component calling useContext will always re-render when the context value changes. If re-rendering the component is expensive, you can optimize it by using memoization
Possible solutions to this also described
I see universal solution is to useMemo
For example
const Example = props => {
const { match, history } = props;
const { userState, userDispatch } = useContext(AppContext);
// Push to user detail route /user/userId
const selectUserList = userId => {
history.push(`/user/${userId}`);
userDispatch.clearTabValue(true);
};
const users = userState.users;
return useMemo(() => {
return <Frame>
<ListMenu
dataList={users}
selectDataList={selectUserList}
/>
<List />
</Frame>
}, [users, selectUserList]); // Here are variables that component depends on
};
I also may recommend you to completly switch to Redux. You're almost there with using combineReducers and dispatch. React-redux now exposes useDispatch and useSelector hooks, so you can make your code very close to what you're doing now (replace useContext with useSelector and useReducer with useDispatch. It will require minor changes to arguments)

Resources