How to save / get a value with the redux store? - reactjs

Hello Stackoverflow Community,
I was hoping you could help me with the following logic. I would like to return from a custom-built hook the last item the user selected in a onClick function.
const useActiveWeb3React = (): Web3ReactContextInterface<Web3Provider> => {
const { chainId, account, ...web3React } = useWeb3React()
const { solanaAccount, solanaChainId } = useSolanaWeb3React()
const activeChain = "if the user's last selected chain is Solana" ? solanaChainId : chainId
const activeAccount = activeChain === ChainId.SOLANA ? solanaAccount : account
return { chainId: activeChain, account: activeAccount, ...web3React }
}
OnClick handler that would send a network change request to either MetaMask or Phantom Wallet when user selects one of the chains.
const handleSelection = (network: ChainId) => {
onDismiss()
onNetworkSelect(network)
}
What I would like to accomplish is that if the user selected in the app ChainId.SOLANA I would like to update activeChain variable in the useActiveWeb3React hook so that the whole app also knows user now wants to be on Solana. And then if the user switches back to Ethereum I want to update the activeChain to reflect the users last selection. Also I would like to stay at that chain if user refreshes the app.
I have access to redux store in the app.
How would you do it?
Thanks for suggestions!

You need three different things.
Save a value inside the state
Get a value inside the state
Persit the state (so when user refresh the page you still have this information)
How to save a value in the redux store ?
To avoid boilerplate redux team recommend to use redux-toolkit as the standard approach.
You can create a part of the state of your app with createSlice
This will help you to create actions creator and reducers
userWallet.slice.ts
import { createSlice } from '#reduxjs/toolkit'
import type { PayloadAction } from '#reduxjs/toolkit'
interface UserWalletState {
chainId: ChainId | null
}
const initialState = { chainId: null }
const userWalletSlice = createSlice({
name: 'userWallet',
initialState,
reducers: {
setChainId(state, action: PayloadAction<ChainId>) {
state.chainId = action.payload
},
},
})
export const { setChainId } = userWalletSlice.actions
export default userWalletSlice.reducer
We create a slice named: userWalletSlice and we export the generated reducer, don't forget to add it as your reducers now:
rootReducer.ts
import userWalletReducer from './userWallet.slice.ts'
export const rootReducer = combineReducers({ userWallet: userWalletReducer })
When create this reducer inside userWallet.slice.ts:
setChainId(state, action: PayloadAction<ChainId>) {
state.chainId = action.payload
},
Every time we dispatch the action setChainId. This will take the payload and update state.chainId.
When we create setChainId reducer this will automatically create an action creator that you can export.
export const { setChainId } = userWalletSlice.actions
You'll be able to dispatch it like this:
dispatch(setChainId(chainId))
Then inside your callback you can just do:
const handleSelection = (network: ChainId) => {
onDismiss();
dispatch(setChainId(network));
}
Now that you can put this chainId into your state, you need to retrieve automatically when it change.
How to get a value from redux store ?
To get values inside the store we use selectors.
You can use createSelector to create a memoized selector.
userWallet.selector.ts
export const userWalletSelector = (state: AppState) => state.userWallet;
export const chainIdSelector = createSelector(userWalletSelector, userWallet => userWallet.chainId)
So now inside your custom hook you can get this value with the following line.
const activeChain = useSelector(chainIdSelector);
How to persist redux state to retrieve it when I refresh a page ?
There are multiple way to persist state of you app. For your kind of information like chainId you can use tool like redux-persist to save part of your state inside the local storage for example

Related

How can I cache data that I already requested and access it from the store using React and Redux Toolkit

How can I get data from the store using React Redux Toolkit and get a cached version if I already requested it?
I need to request multiple users for example user1, user2, and user3. If I make a request for user1 after it has already been requested then I do not want to fetch user1 from the API again. Instead it should give me the info of the user1 from the store.
How can I do this in React with a Redux Toolkit slice?
Edit: This answer predates the release of RTK Query which has made this task much easier! RTK Query automatically handles caching and much more. Check out the docs for how to set it up.
Keep reading if you are interested in understanding more about some of the concepts at play.
Tools
Redux Toolkit can help with this but we need to combine various "tools" in the toolkit.
createEntityAdapter allows us to store and select entities like a user object in a structured way based on a unique ID.
createAsyncThunk will create the thunk action that fetches data from the API.
createSlice or createReducer creates our reducer.
React vs. Redux
We are going to create a useUser custom React hook to load a user by id.
We will need to use separate hooks in our hooks/components for reading the data (useSelector) and initiating a fetch (useDispatch). Storing the user state will always be the job of Redux. Beyond that, there is some leeway in terms of whether we handle certain logic in React or in Redux.
We could look at the selected value of user in the custom hook and only dispatch the requestUser action if user is undefined. Or we could dispatch requestUser all the time and have the requestUser thunk check to see if it needs to do the fetch using the condition setting of createAsyncThunk.
Basic Approach
Our naïve approach just checks if the user already exists in the state. We don't know if any other requests for this user are already pending.
Let's assume that you have some function which takes an id and fetches the user:
const fetchUser = async (userId) => {
const res = await axios.get(`https://jsonplaceholder.typicode.com/users/${userId}`);
return res.data;
};
We create a userAdapter helper:
const userAdapter = createEntityAdapter();
// needs to know the location of this slice in the state
export const userSelectors = userAdapter.getSelectors((state) => state.users);
export const { selectById: selectUserById } = userSelectors;
We create a requestUser thunk action creator that only executes the fetch if the user is not already loaded:
export const requestUser = createAsyncThunk("user/fetchById",
// call some API function
async (userId) => {
return await fetchUser(userId);
}, {
// return false to cancel
condition: (userId, { getState }) => {
const existing = selectUserById(getState(), userId);
return !existing;
}
}
);
We can use createSlice to create the reducer. The userAdapter helps us update the state.
const userSlice = createSlice({
name: "users",
initialState: userAdapter.getInitialState(),
reducers: {
// we don't need this, but you could add other actions here
},
extraReducers: (builder) => {
builder.addCase(requestUser.fulfilled, (state, action) => {
userAdapter.upsertOne(state, action.payload);
});
}
});
export const userReducer = userSlice.reducer;
But since our reducers property is empty, we could just as well use createReducer:
export const userReducer = createReducer(
userAdapter.getInitialState(),
(builder) => {
builder.addCase(requestUser.fulfilled, (state, action) => {
userAdapter.upsertOne(state, action.payload);
});
}
)
Our React hook returns the value from the selector, but also triggers a dispatch with a useEffect:
export const useUser = (userId: EntityId): User | undefined => {
// initiate the fetch inside a useEffect
const dispatch = useDispatch();
useEffect(
() => {
dispatch(requestUser(userId));
},
// runs once per hook or if userId changes
[dispatch, userId]
);
// get the value from the selector
return useSelector((state) => selectUserById(state, userId));
};
isLoading
The previous approach ignored the fetch if the user was already loaded, but what about if it is already loading? We could have multiple fetches for the same user occurring simultaneously.
Our state needs to store the fetch status of each user in order to fix this problem. In the docs example we can see that they store a keyed object of statuses alongside the user entities (you could also store the status as part of the entity).
We need to add an empty status dictionary as a property on our initialState:
const initialState = {
...userAdapter.getInitialState(),
status: {}
};
We need to update the status in response to all three requestUser actions. We can get the userId that the thunk was called with by looking at the meta.arg property of the action:
export const userReducer = createReducer(
initialState,
(builder) => {
builder.addCase(requestUser.pending, (state, action) => {
state.status[action.meta.arg] = 'pending';
});
builder.addCase(requestUser.fulfilled, (state, action) => {
state.status[action.meta.arg] = 'fulfilled';
userAdapter.upsertOne(state, action.payload);
});
builder.addCase(requestUser.rejected, (state, action) => {
state.status[action.meta.arg] = 'rejected';
});
}
);
We can select a status from the state by id:
export const selectUserStatusById = (state, userId) => state.users.status[userId];
Our thunk should look at the status when determining if it should fetch from the API. We do not want to load if it is already 'pending' or 'fulfilled'. We will load if it is 'rejected' or undefined:
export const requestUser = createAsyncThunk("user/fetchById",
// call some API function
async (userId) => {
return await fetchUser(userId);
}, {
// return false to cancel
condition: (userId, { getState }) => {
const status = selectUserStatusById(getState(), userId);
return status !== "fulfilled" && status !== "pending";
}
}
);

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 practice for combining React useContext and one-time data load (React hooks)?

I have a form composed of several input components. The form data is shared and shareable across these sibling components via a React context and the React hook useContext.
I am stumbling on how to optionally async load data into the same context. For example, if the browser URL is example.com/form, then the form can load with the default values provided by a FormContext (no problem). But if the user is returning to finish a previously-edited form by navigating to example.com/form/:username/:form-id, then application should fetch the data using those two data points. Presumably this must happen within the FormContext somehow, in order to override the default empty form initial value.
Are url params even available to a React.createContext function?
If so, how to handle the optional data fetch requirement when hooks are not to be used with in a conditional?
How to ensure that the data fetch occurs only once?
Lastly, the form also saves current state to local storage in order to persist values on refresh. If the data fetch is implemented, should a refresh load the async data or the local storage? I'm leaning towards local storage because that is more likely to represent what the user last experienced. I'm open to opinions and thoughts on implementation.
FormContext
export const FormContext = React.createContext();
export const FormProvider = props => {
const defaultFormValues = {
firstName: "",
lastName: "",
whatever: "",
};
const [form, setForm] = useLocalStorage(
"form",
defaultFormValues
);
return (
<FormContext.Provider value={{ form, setForm }}>
{props.children}
</FormContext.Provider>
);
};
Reference for useLocalStorage
I think the answer you're looking for is Redux, not the library but the workflow. I did find it curious React doesn't give more guidance on this. I'm not sure what others are doing but this is what I came up with.
First I make sure the dispatch from useReducer is added to the context. This is the interface for that:
export interface IContextWithDispatch<T> {
context: T;
dispatch: Dispatch<IAction>;
}
Then given this context:
export interface IUserContext {
username: string;
email: string;
password: string;
isLoggedIn: boolean;
}
I can do this:
export const UserContext = createContext<IContextWithDispatch<IUserContext>>({
context: initialUserContext,
dispatch: () => {
return initialUserContext;
},
});
In my top level component I memoize the context because I only want one instance. This is how I put it all together
import memoize from 'lodash/memoize';
import {
IAction,
IContextWithDispatch,
initialUserContext,
IUserContext,
} from './domain';
const getCtx = memoize(
([context, dispatch]: [IUserContext, React.Dispatch<IAction>]) =>
({ context, dispatch } as IContextWithDispatch<IUserContext>),
);
const UserProvider = ({ children }) => {
const userContext = getCtx(useReducer(userReducer, initialUserContext)) as IContextWithDispatch<
IUserContext
>;
useEffect(() => {
// api call to fetch user info
}, []);
return <UserContext.Provider value={userContext}>{children}</UserContext.Provider>;
};
Your userReducer will be responding to all dispatch calls and can make API calls or call another service to do that etc... The reducer handles all changes to the context.
A simple reducer could look like this:
export default (user, action) => {
switch (action.type) {
case 'SAVE_USER':
return {
...user,
isLoggedIn: true,
data: action.payload,
}
case 'LOGOUT':
return {
...user,
isLoggedIn: false,
data: {},
}
default:
return user
}
}
In my components I can now do this:
const { context, dispatch } = useContext<IContextWithDispatch<IUserContext>>(UserContext);
where UserContext gets imported from the export defined above.
In your case, if your route example.com/form/:username/:form-id doesn't have the data it needs it can dispatch an action and listen to the context for the results of that action. Your reducer can make any necessary api calls and your component doesn't need to know anything about it.
Managed to accomplish most of what I wanted. Here is a gist demonstrating the basic ideas of the final product: https://gist.github.com/kwhitejr/df3082d2a56a00b7b75365110216b395
Happy to receive feedback!

What is the best way of keeping database in sync with the Redux store (redux middleware)?

So I have a goal to keep my MongoDB database in sync with the current state of my application. For example, whenever my state changes (like the name of a project, for example), that change will be saved into the database after the action is dispatched and reducer is signaled and the state is changed.
As an example, I have a state object with the following structure:
const INITIAL_STATE = {
name: "",
triggered: false,
isUnique: false
};
So when a user changes the name of the project, that name change will be first be done by the state itself and after the state changes, there is a call to the DB to just change the name of the project.
To simulate a DB change, I used localStorage to get across the same purpose:
function handshake() {
return ({ dispatch, getState }) => next => action => {
// send action to next Middleware
next(action);
const db = JSON.parse(localStorage.getItem("temporaryDB"));
const presentState = getCurrentState(getState());
if(db) {
const areStatesEqual = isEqual(db, presentState);
if(!areStatesEqual) return localStorage.setItem("temporaryDB", JSON.stringify(presentState));
return;
}
localStorage.setItem("temporaryDB", JSON.stringify(presentState));
};
}
export default function configureStore(initialState = {}) {
return createStore(
rootReducer,
applyMiddleware(handshake())
)
}
getCurrentState is just a utility function that gets the current state. Regardless, my logic is to use a Redux middleware and look for changes between the database object and the store object. If the objects are different in any way, I would replace the DB object with the Redux store, keeping everything in sync.
It's a naive approach and I'm looking to see if there is a better way of reaching the goal I have of keeping the state and database in sync throughout the entire application's lifecylce.
I think you just need to subscribe to the store and listen to all the changes happening there.
e.g. two function to load/save state to keep it sync
export const loadState = () => {/*the DB logic*/}
export const saveState= () => {/*the DB logic*/}
then you can compose your redux with these function and initial the state with calling the loadState()
import { loadState, saveState } from "where theyare"
const syncWithDBstate= loadState();
const store = createStore(
rootReducer,
syncWithDBstate,
composeWithDevTools(applyMiddleware(thunk)) // here I am suing the chrome devtool extention
);
store.subscribe(() => {
saveState(store.getState());
});

React-native + Redux(Combine Reducers): Create state structure

I am developing a react-native app and new to redux. I've 3 screens and almost all state of all screens dependent on each other.
e.g First screen scans the product, second screen takes customer details and third screen displays the summary from of products and customer.
I am hitting the wall since 2 days to create the state structure and I end up with combineReducers. But in combine reducers, each reducer maintains its own slice of state and doesn't let me access or update the state of another reducer.
So,
Q1. Should I continue with combieReducers or should maintain single reducer only?
Q2. If combineReducers is OK, how should access or update the same state along all reducers?
for u r first question yes u have to combine because combineReducers will give all reducers data in single object
example:
const rootReducer = combineReducers({
posts: PostsReducer,
form: formReducer
});
inside component
function mapStateToProps(state) {
return { posts: state.posts };
}
export default connect(mapStateToProps, { createPost })(PostsIndex);
in the above code through Redux connect you can access state which is produced by combine reducera like this.props.posts inside your component
To update the you need to triger an action which go through all reducers and based on type you can update the state
example:
export function createPost(values, callback) {
const request = axios
.post(`${ROOT_URL}/posts${API_KEY}`, values)
.then(() => callback());
return {
type: CREATE_POST,
payload: request
};
}
using middleware you can achieve the funtionality
export default ({ dispatch, getState }) => next => action => {
next(action);
const newAction = { ...action, payload: "",state:getState () };
dispatch(newAction);
};

Resources