Using Redux default store from window in React app - javascript

I have an React Web App that collects user saved information from local storage. First I retrieve saved information to window.store. I can see in console that window.store is there. But it always gives undefined error. I tried to make sleep so that after sleep, store can get data from global window object.
init.js
const InitializeApp = () => {
let [getState, setState] = useState(false)
useEffect(()=>{
const asyncState = async() => {
let store = await loadFromStorage()
window.store = store
await sleep(5000)
console.log(store);
setState(true)
}
asyncState()
},[])
if(!getState){
return(
<InitTemplate />
)
}else{
return(
<Main />
)
}
}
App.js
const Main = () => {
return(
<Provider store={Store}>
<App />
</Provider>
);
}
store.js
const AppReducer = (state = window.store , action) => {
switch(action.type){
default :
return state;
}
}
Problem is I am getting undefined whenever I use useSelector(). How can I achieve this ?

Apparently you are trying to save the state to the localStorage and load it after reloading the page
I think that you do not need to create a Promise at the moment when you load the state from the localeStorage
Try to initialize the store as shown below
store.js
import {createStore} from "redux";
import AppReducer from "./AppReducer";
export const loadState = () => {
try {
const state = localStorage.getItem('state');
if (state === null) {
return undefined;
}
return JSON.parse(state);
} catch (err) {
return undefined;
}
};
export const saveState = (state) => {
try {
const st = JSON.stringify(state);
localStorage.setItem('state', st);
} catch (err) {
}
};
const throttle = (func, limit) => {
let lastFunc;
let lastRan;
return function() {
const context = this;
const args = arguments;
if (!lastRan) {
func.apply(context, args);
lastRan = Date.now();
} else {
clearTimeout(lastFunc);
lastFunc = setTimeout(function() {
if ((Date.now() - lastRan) >= limit) {
func.apply(context, args);
lastRan = Date.now()
}
}, limit - (Date.now() - lastRan))
}
}
};
const store = createStore(AppReducer, loadState());
store.subscribe(throttle(() => {
// This callback is called on every state change.
// Here your current state is written to the localStorage
saveState(store.getState());
}, 2000));
export default store;

Related

What is the reason getState() not functioning in React-Redux?

export const SMSAdmin_Filter_User = (userName) => (dispatch, getState) => {
var st = getState();
...
}
When this code runs, getState() is defined in the debugger as a function, but st comes up as undefined. I have used getState in multiple other action creator functions with great success, so am uncertain why it is not functioning here.
This function is called as a promise since there are other asynchronous processes running (incremental fetch for large number of users).
Here is where it is being called from:
componentDidMount() {
var el = document.getElementById("userList");
if (el) {
el.focus({ preventScroll: true });
}
// console.log("SMSAdmin")
new Promise((res,rej)=>this.props.SMSAdmin_Get_Users());
// .then(() => {
// this.setState({ users: this.props.SMSAdmin.users });
// });
}
filterUsers = () => {
let target = document.getElementById("userName");
let name = target?.value?.toLowerCase();
new Promise((res, rej)=>SMSAdmin_Filter_User(name?.trim()));
}
filterUsers() is also being called from the render function to ensure updates when SMSAdmin_Get_Users() adds more users to the list.
Here is the mapDispatchToProps():
const mapDispatchToProps = (dispatch) => {
return {
SMSAdmin_Get_Users: () => { return dispatch(SMSAdmin_Get_Users()) },
SMSAdmin_Load_User: (userId, userName, oldData = null, startVal = 0, number = 20) => {
return dispatch(SMSAdmin_Load_User(userId, userName, oldData, startVal, number))
},
SMSAdmin_Update_User: (user, province, credits) => { return dispatch(SMSAdmin_Update_User(user, province, credits)) },
SMSAdmin_setCurrentUpload: (userName) => { return dispatch(SMSAdmin_setCurrentUpload(userName)) },
SMSAdmin_Filter_User: (userName) => { return dispatch(SMSAdmin_Filter_User(userName)) },
}
}
I am not able to provide a sandbox for the code because there are multiple other files associated with this component and the data being used is confidential.
Thanks.
Edit: Showing redux store creation
import { createStore, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import logger from 'redux-logger';
import { reducers } from './reducerMain.js';
export const ConfigureStore = () => {
const store = createStore(
reducers,
applyMiddleware(thunk, logger)
);
return store;
}
I guess you are accidently using imported function (not the one mapped in mapDispatchToProps). Did you forgot to use the one from props ? like that:
filterUsers = () => {
// ...
new Promise((res, rej)=>this.props.SMSAdmin_Filter_User(name?.trim()));
}

Redux is not updating component even after returning new state object

I am new to redux.
What I am trying to do is to fetch auth status from redux state and accordingly render my components if auth is true then render home else login but every time redux is returning undefined as auth state.
This is my component
import axios from 'axios';
import jsonwebtoken from 'jsonwebtoken';
import Login from './Containers/Login'
import Home from './Containers/Home'
import {connect } from 'react-redux'
const server = 'http://localhost:5000/api/';
function getCookie(cname) {
let name = cname + "=";
let ca = document.cookie.split(';');
for (let i = 0; i < ca.length; i++) {
let c = ca[i];
while (c.charAt(0) === ' ') {
c = c.substring(1);
}
if (c.indexOf(name) === 0) {
return c.substring(name.length, c.length);
}
}
return "";
}
function App({auth, setstate}) {
useEffect(()=>{
const fetchdata = async ()=>{
const token = getCookie('token');
let loggedInUser = {};
if (token !== '') {
try {
loggedInUser = await jsonwebtoken.verify(token, 'shhhhh')._doc;
const res1 = await axios.post(server + 'userlist/getlist', { id: loggedInUser._id });
const res2 = await axios.post(server + 'usertodo/gettodo', { listid: loggedInUser.defaultListid });
if (res1.data.error === null && res2.data.error === null) {
const { lists } = res1.data;
const { todos } = res2.data;
const newState = {
isAuth: true,
loggedInUser,
selectedListid: loggedInUser.defaultListid,
todos,
lists
}
setstate(newState)
}
else{
console.log(res1.data.error, res2.data.error);
setstate({
isAuth:false
})
}
} catch (err) {
console.log(err)
}
}
}
fetchdata();
},[setstate, auth])
return (auth ? <Home/>:<Login/>);
}
const mapStateToProps = state =>{
return {
auth: state.isAuth
}
}
const mapDispatchToProps = dispatch =>{
return {
setstate: async(newState)=> dispatch({type:'SET_STATES', newState})
}
}
export default connect(mapStateToProps, mapDispatchToProps)(App);
=========================================================================
this is my initialState
isAuth: false,
loggedInUser: null,
selectedListid: null,
todos: [],
lists: []
}
and reducer
switch (action.type) {
case 'SET_STATES':
{
const { newState } = action
return {
...state,
...newState
}
}
default:
return state;
I would advise you to use thunk instead of having all the async logic in your component and instead of using connect you can use the useSelector and useDispatch hooks from react-redux. You should also make action creator functions and store action types in constants. Having only a SET_STATE action that sets the entire state defeats the purpose of redux, try to create actions that do a specific thing.
I am not getting undefined as auth state using your exact code so there must be something missing in your question. Did you try checking the redux devtools and can you provide us with the information there like what is the state, what actions are dispatched, what changes did they make to the state? There may be something wrong with the fetch code as well so it'll end up in catch but then nothing should happen and you auth should not be undefined as it is set in initial state.
If you still have trouble getting your code to work properly then I suggest adding a runnable snippet to your question that reproduces the problem or create a sandbox
Below is your redux code working as expected, I added an extra check if auth is false so it won't authenticate when you are already authenticated:
const { Provider, connect } = ReactRedux;
const { createStore, applyMiddleware, compose } = Redux;
const initialState = {
isAuth: false,
};
const reducer = (state, action) => {
switch (action.type) {
case 'SET_STATES': {
const { newState } = action;
return {
...state,
...newState,
};
}
default:
return state;
}
};
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
() => (next) => (action) => next(action)
)
)
);
//fake getCookie
const getCookie = () => 'token';
function App({ auth, setstate }) {
React.useEffect(() => {
//removed async because SO has ancient babel
const fetchdata = () => {
const token = getCookie('token');
if (token !== '' && auth === false) {
//only do this if not authenticated
const newState = {
isAuth: true,
};
setstate(newState);
}
};
fetchdata();
}, [setstate, auth]);
return <div>auth is: {String(auth)}</div>;
}
const mapStateToProps = (state) => {
return {
auth: state.isAuth,
};
};
const mapDispatchToProps = (dispatch) => {
return {
//removed async because SO as ancient babel
setstate: (newState) =>
dispatch({ type: 'SET_STATES', newState }),
};
};
const ConnectedApp = connect(
mapStateToProps,
mapDispatchToProps
)(App);
ReactDOM.render(
<Provider store={store}>
<ConnectedApp />
</Provider>,
document.getElementById('root')
);
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/16.8.4/umd/react.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/16.8.4/umd/react-dom.production.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/redux/4.0.5/redux.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-redux/7.2.0/react-redux.min.js"></script>
<div id="root"></div>

Using React Context with AsyncStorage using hooks

My goal is to use custom hooks created from Context to pass and modify stored values
The final goal is to use something like useFeedContext() to get or modify the context values
What I am actually getting is either the functions that I call are undefined or some other problem ( I tried multiple approaches)
I tried following this video basics of react context in conjunction with this thread How to change Context value while using React Hook of useContext but I am clearly getting something wrong.
Here is what I tried :
return part of App.js
<FeedProvider mf={/* what do i put here */}>
<Navigation>
<HomeScreen />
<ParsedFeed />
<FavScreen />
</Navigation>
</FeedProvider>
Main provider logic
import React, { useState, useEffect, useContext, useCallback } from "react";
import AsyncStorage from "#react-native-async-storage/async-storage";
const FeedContext = React.createContext();
const defaultFeed = [];
const getData = async (keyName) => {
try {
const jsonValue = await AsyncStorage.getItem(keyName);
return jsonValue != null ? JSON.parse(jsonValue) : null;
} catch (e) {
console.log(e);
}
};
const storeData = async (value, keyName) => {
console.log(value, keyName);
try {
const jsonValue = JSON.stringify(value);
await AsyncStorage.setItem(keyName, jsonValue);
} catch (e) {
console.log(e);
}
};
export const FeedProvider = ({ children, mf }) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
const [feedLoaded, setFeedLoaded] = useState(false);
let load = async () => {
let temp = await AsyncStorage.getItem("mainUserFeed");
temp != null
? getData("mainUserFeed").then((loadedFeed) => setMainFeed(loadedFeed))
: setMainFeed(defaultFeed);
setFeedLoaded(true);
};
useEffect(() => {
load();
}, []);
useCallback(async () => {
if (!feedLoaded) {
return await load();
}
}, [mainFeed]);
const setFeed = (obj) => {
setMainFeed(obj);
storeData(mainFeed, "mainUserFeed");
};
return (
<FeedContext.Provider value={{ getFeed: mainFeed, setFeed }}>
{children}
</FeedContext.Provider>
);
};
//export const FeedConsumer = FeedContext.Consumer;
export default FeedContext;
The custom hook
import { useContext } from "react";
import FeedContext from "./feedProviderContext";
export default function useFeedContext() {
const context = useContext(FeedContext);
return context;
}
What I would hope for is the ability to call the useFeedContext hook anywhere in the app after import like:
let myhook = useFeedContext()
console.log(myhook.getFeed) /// returns the context of the mainFeed from the provider
myhook.setFeed([{test:1},{test:2}]) /// would update the mainFeed from the provider so that mainFeed is set to the passed array with two objects.
I hope this all makes sense, I have spend way longer that I am comfortable to admit so any help is much appreciated.
If you want to keep using your useFeedContext function, I suggest to move it into the your 'Provider Logic' or I'd call it as 'FeedContext.tsx'
FeedContext.tsx
const FeedContext = createContext({});
export const useFeedContext = () => {
return useContext(FeedContext);
}
export const AuthProvider = ({children}) => {
const [mainFeed, setMainFeed] = useState(mf || defaultFeed);
...
return (
<FeedContext.Provider value={{mainFeed, setMainFeed}}>
{children}
</FeedContext.Provider>
);
};
YourScreen.tsx
const YourScreen = () => {
const {mainFeed, setMainFeed} = useFeedContext();
useEffect(() => {
// You have to wait until mainFeed is defined, because it's asynchronous.
if (!mainFeed || !mainFeed.length) {
return;
}
// Do something here
...
}, [mainFeed]);
...
return (
...
);
};
export default YourScreen;

Preserve state value on client side navigation - NextJs - Next-Redux-Wrapper

So I am trying to fix the hydrating issue I am facing when using wrapper.getServerSideProps. When I reroute with the current setup the store is cleared out and then the new data is added, which results in a white page since a lot of important data is no longer there (i.e, translations and cms data).
Screenshot from redux-dev-tools Hydrate action diff:
Screenshot is taken after routing from the homepage to a productpage, so that there was an existing store. Everything is reset to the initial app state.
What I am trying to do
In the store.js I create the store and foresee a reducer to handle the Hydrate call. The downside of this approach is that the payload will always be a new store object since it is called on the server. I was thinking to check the difference between the 2 json's and then only apply the difference instead of the whole initial store.
Get the difference between the client and server state.
Make the next state, overwrite clientstate with patched serverstate so this includes, updated state from hydrate and the existing client state.
Currently results in a white page.
You can see the reducer code below in the store.js
//store.js
import combinedReducer from './reducer';
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const clientState = { ...state };
const serverState = { ...action.payload };
if (state) {
// preserve state value on client side navigation
// Get the difference between the client and server state.
const diff = jsondiffpatch.diff(clientState, serverState);
if (diff !== undefined) {
// If there is a diff patch the serverState, with the existing diff
jsondiffpatch.patch(serverState, diff);
}
}
// Make next state, overwrite clientstate with patched serverstate
const nextState = {
...clientState,
...serverState,
};
// Result, blank page.
return nextState;
}
return combinedReducer(state, action);
};
export const makeStore = () => {
const cookies = new Cookies();
const client = new ApiClient(null, cookies);
const middleware = [
createMiddleware(client),
thunkMiddleware.withExtraArgument(cookies),
];
return createStore(reducer, bindMiddleware(middleware));
};
const wrapper = createWrapper(makeStore);
export default wrapper;
//_app.jsx
const App = (props) => {
const { Component, pageProps, router } = props;
return (
<AppComponent cookies={cookies} locale={router.locale} location={router}>
<Component {...pageProps} />
</AppComponent>
);
};
App.getInitialProps = async ({ Component, ctx }) => {
return {
pageProps: {
...(Component.getInitialProps ? await Component.getInitialProps(ctx) : {}),
},
};
};
App.propTypes = {
Component: PropTypes.objectOf(PropTypes.any).isRequired,
pageProps: PropTypes.func,
router: PropTypes.objectOf(PropTypes.any).isRequired,
};
App.defaultProps = {
pageProps: () => null,
};
export default wrapper.withRedux(withRouter(App));
// Product page
export const getServerSideProps = wrapper.getServerSideProps(
async ({ query, store: { dispatch } }) => {
const productCode = query.id?.split('-', 1).toString();
await dispatch(getProductByCode(productCode, true));
});
const PDP = () => {
const { product } = useSelector((state) => state.product);
return (
<PageLayout>
<main>
<h1>{product?.name}</h1>
<div
className="description"
dangerouslySetInnerHTML={{ __html: product?.description }}
/>
</main>
</PageLayout>
);
};
export default PDP;
Oke, so I solved my issue through not overthinking the concept. Went back to the drawing board and made a simple solution.
Came to the conclusion that there are only a few state objects that need to persist during client navigation.
I only had to make a change to my i18n, to make it dynamic since we fetch translations on page basis.
This is the final reducer for anyone that might, in the future run into a similar problem.
const reducer = (state, action) => {
if (action.type === HYDRATE) {
const clientState = { ...state };
const serverState = { ...action.payload };
const nextState = { ...clientState, ...serverState };
const locale = nextState.i18n.defaultLocale || config.i18n.defaultLocale;
const nextI18n = {
...state.i18n,
locale,
messages: {
[locale]: {
...state.i18n.messages[locale],
...nextState.i18n.messages[locale],
},
},
loadedGroups: {
...state.i18n.loadedGroups,
...nextState.i18n.loadedGroups,
},
};
if (state) {
nextState.i18n = nextI18n;
nextState.configuration.webConfig = state.configuration.webConfig;
nextState.category.navigation = state.category.navigation;
}
return nextState;
}
return combinedReducer(state, action);
};

createAsyncThunk: abort previous request

I'm using createAsyncThunk to make asynchronous requests to some API. Only one request should be active at any given moment.
I understand that the request can be aborted using a provided AbortSignal if I have the Promise returned from the previous invocation. Question is, can the thunk itself somehow abort the previous request "autonomously"?
I was considering two options:
keeping the last AbortSignal in the state. Seems wrong, because state should be serializable.
keeping the last Promise and its AbortSignal in global variable. Seems also wrong, because, you know, global variables.
Any ideas? Thank you.
I don't know how your specific api works but below is a working example of how you can put the abort logic in the action and reducer, it will abort any previously active fake fetch when a newer fetch is made:
import * as React from 'react';
import ReactDOM from 'react-dom';
import {
createStore,
applyMiddleware,
compose,
} from 'redux';
import {
Provider,
useDispatch,
useSelector,
} from 'react-redux';
import {
createAsyncThunk,
createSlice,
} from '#reduxjs/toolkit';
const initialState = {
entities: [],
};
// constant value to reject with if aborted
const ABORT = 'ABORT';
// fake signal constructor
function Signal() {
this.listener = () => undefined;
this.abort = function () {
this.listener();
};
}
const fakeFetch = (signal, result, time) =>
new Promise((resolve, reject) => {
const timer = setTimeout(() => resolve(result), time);
signal.listener = () => {
clearTimeout(timer);
reject(ABORT);
};
});
// will abort previous active request if there is one
const latest = (fn) => {
let previous = false;
return (signal, result, time) => {
if (previous) {
previous.abort();
}
previous = signal;
return fn(signal, result, time).finally(() => {
//reset previous
previous = false;
});
};
};
// fake fetch that will abort previous active is there is one
const latestFakeFetch = latest(fakeFetch);
const fetchUserById = createAsyncThunk(
'users/fetchByIdStatus',
async ({ id, time }) => {
const response = await latestFakeFetch(
new Signal(),
id,
time
);
return response;
}
);
const usersSlice = createSlice({
name: 'users',
initialState: { entities: [], loading: 'idle' },
reducers: {},
extraReducers: {
[fetchUserById.fulfilled]: (state, action) => {
state.entities.push(action.payload);
},
[fetchUserById.rejected]: (state, action) => {
if (action?.error?.message === ABORT) {
//do nothing
}
},
},
});
const reducer = usersSlice.reducer;
//creating store with redux dev tools
const composeEnhancers =
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ || compose;
const store = createStore(
reducer,
initialState,
composeEnhancers(
applyMiddleware(
({ dispatch, getState }) => (next) => (action) =>
typeof action === 'function'
? action(dispatch, getState)
: next(action)
)
)
);
const App = () => {
const dispatch = useDispatch();
React.useEffect(() => {
//this will be aborted as soon as the next request is made
dispatch(
fetchUserById({ id: 'will abort', time: 200 })
);
dispatch(fetchUserById({ id: 'ok', time: 100 }));
}, [dispatch]);
return 'hello';
};
ReactDOM.render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
);
If you only need to resolve a promise if it was the latest requested promise and no need to abort or cancel ongoing promises (ignore resolve if it wasn't latest) then you can do the following:
const REPLACED_BY_NEWER = 'REPLACED_BY_NEWER';
const resolveLatest = (fn) => {
const shared = {};
return (...args) => {
//set shared.current to a unique object reference
const current = {};
shared.current = current;
fn(...args).then((resolve) => {
//see if object reference has changed
// if so it was replaced by a newer one
if (shared.current !== current) {
return Promise.reject(REPLACED_BY_NEWER);
}
return resolve;
});
};
};
How it's used is demonstrated in this answer
Based on #HMR answer, I was able to put this together, but it's quite complicated.
The following function creates "internal" async thunk that does the real job, and "outer" async thunk that delegates to the internal one and aborts previous dispatches, if any.
The payload creator of the internal thunk is also wrapped to: 1) wait for the previous invocation of payload creator to finish, 2) skip calling the real payload creator (and thus the API call) if the action was aborted while waiting.
import { createAsyncThunk, AsyncThunk, AsyncThunkPayloadCreator, unwrapResult } from '#reduxjs/toolkit';
export function createNonConcurrentAsyncThunk<Returned, ThunkArg>(
typePrefix: string,
payloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg>,
options?: Parameters<typeof createAsyncThunk>[2]
): AsyncThunk<Returned, ThunkArg, unknown> {
let pending: {
payloadPromise?: Promise<unknown>;
actionAbort?: () => void;
} = {};
const wrappedPayloadCreator: AsyncThunkPayloadCreator<Returned, ThunkArg> = (arg, thunkAPI) => {
const run = () => {
if (thunkAPI.signal.aborted) {
return thunkAPI.rejectWithValue({name: 'AbortError', message: 'Aborted'});
}
const promise = Promise.resolve(payloadCreator(arg, thunkAPI)).finally(() => {
if (pending.payloadPromise === promise) {
pending.payloadPromise = null;
}
});
return pending.payloadPromise = promise;
}
if (pending.payloadPromise) {
return pending.payloadPromise = pending.payloadPromise.then(run, run); // don't use finally(), replace result
} else {
return run();
}
};
const internalThunk = createAsyncThunk(typePrefix + '-protected', wrappedPayloadCreator);
return createAsyncThunk<Returned, ThunkArg>(
typePrefix,
async (arg, thunkAPI) => {
if (pending.actionAbort) {
pending.actionAbort();
}
const internalPromise = thunkAPI.dispatch(internalThunk(arg));
const abort = internalPromise.abort;
pending.actionAbort = abort;
return internalPromise
.then(unwrapResult)
.finally(() => {
if (pending.actionAbort === abort) {
pending.actionAbort = null;
}
});
},
options
);
}

Categories

Resources