Sorry for bad English.
I'm struggling with ssr(pure react) + redux + code splitting(#loadable) + injecting redux. (highly effected by react-boilerplate)
currently my code working great without preload data.
I don't know how can I handle ssr preload data before inject reducer.
here is example to help my problem is.
store = {
global: { // default
key: 'value' // this is done. ssr working great using this value.
},
injected: { // dynamically injected. using replaceReducer per page. (same with react-boilerplate)
key: 'value' // I want to put this value on ssr preload. (not working)
}
}
When it done, it said
Unexpected property "injected" found in previous state received by the reducer. Expected to find one of the known reducer property names instead: "global". Unexpected properties will be ignored.
I know why this error comes(because initial store does not has 'injected' store.), but I don't know How can I fix it properly.
Is there any usage example?
Here is my thought, but it seemed not proper answer.
insert key for preload data on 'global'.
put preload data on 'global' in server.
Move global to injected store(in this case, 'injected') when injecting is done.
voila!
reducerInjector.js
export const injectState = (reducers, preloadedState = {}) =>
Object.keys(reducers).reduce((result, key) => {
const finalReducers = result;
if (typeof reducers[key] === 'function') {
finalReducers[key] = (state = preloadedState[key], action) => reducers[key](state, action);
}
return finalReducers;
}, {});
export const createInitialState = (reducers, preloadedState = {}) =>
Object.keys(preloadedState).reduce((r, key) => {
if (!reducers[key]) return r;
return { ...r, [key]: preloadedState[key] };
}, {});
export const createReducer = (staticReducers, asyncReducers, preloadedState) =>
combineReducers(injectState({
...staticReducers,
...asyncReducers,
}, preloadedState));
export default function reducerInjector(store, staticReducers, preloadedState) {
// Add a dictionary to keep track of the registered async reducers
store.asyncReducers = {};
// Create an inject reducer function
// This function adds the async reducer, and creates a new combined reducer
store.injectReducer = (key, asyncReducer) => {
store.asyncReducers[key] = asyncReducer;
store.replaceReducer(createReducer(staticReducers, store.asyncReducers, preloadedState));
};
return store;
}
...
import reducerInjector, { createReducer, createInitialState } from './reducerInjector';
const configureStore = (initialState, ssr) => {
const sagaMiddleware = createSagaMiddleware({});
const createStoreWithMiddleware = compose(applyMiddleware(thunkMiddleware, sagaMiddleware));
const reducer = createReducer(rootReducer, {}, initialState);
const initialData = createInitialState(rootReducer, initialState);
let store = createStoreWithMiddleware(createStore)(reducer, initialData);
store = reducerInjector(store, rootReducer, initialState);
return store;
}
Related
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.
When I use useSelector the variable is always holding its initial state. I have the feeling it is stored in some parallel galaxy and never updated. But when I retrieve the value with const store = useStore(); store.getState()... it gives the correct value (but lacks subscribtions). When I inspect the store in redux devtools I can see all the values are recorded in the store correctly. Values are just not retrieved from the store with useSelector.
What I wanted to achieve is to have some cache for user profiles, i.e. not fetch /api/profile/25 multiple times on the same page. I don't want to think of it as "caching" and make multiple requests just keeping in mind the requests are cached and are cheap but rather thinking of it as getting profiles from the store and keeping in mind profiles are fetched when needed, I mean some lazy update.
The implementation should look like a hook, i.e.
// use pattern
const client = useProfile(userId);
// I can also put console.log here to see if the component is getting updated
let outputProfileName;
if( client.state==='pending' ) {
outputProfileName = 'loading...';
} else if( client.state==='succeeded' ) {
outputProfileName = <span>{client.data.name}</span>
} // ... etc
so I placed my code in use-profile.js, having redux-toolkit slice in profile-slice.js
profile-slice.js
import {
createSlice,
//createAsyncThunk,
} from '#reduxjs/toolkit';
const entityInitialValue = {
data: undefined,
state: 'idle',
error: null
};
export const slice = createSlice({
name: 'profile',
initialState: {entities:{}},
reducers: {
updateData: (state,action) => {
// we received data, update the data and the status to 'succeeded'
state.entities[action.payload.id] = {
...entityInitialValue,
//...state.entities[action.payload.id],
data: action.payload.data,
state: 'succeeded',
error: null
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
dispatchPendStart: (state,action) => {
// no data - indicates we started fetching
state.entities[action.payload.id] = {
...entityInitialValue,
//...state.entities[action.payload.id],
data: null,
state: 'pending',
error: null
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
dispatchError: (state,action) => {
state.entities[action.payload.id] = {
//...entityInitialValue,
...state.entities[action.payload.id],
data: null,
state: 'failed',
error: action.payload.error
};
return; // I tried the other approach - return {...state,entities:{...state.entities,[action.payload.id]:{...}}} - both are updating the store, didn't notice any difference
},
},
extraReducers: {
}
});
export const {updateData,dispatchPendStart,dispatchError} = slice.actions;
// export const selectProfile... not used
export default slice.reducer;
use-profile.js
import React, { useState, useEffect } from 'react';
import { useDispatch, useSelector, useStore } from 'react-redux';
import {
updateData as actionUpdateData,
dispatchPendStart as actionDispatchPendStart,
dispatchError as actionDispatchError,
} from './profile-slice';
//import api...
function useProfile(userId) {
const dispatch = useDispatch();
const actionFunction = async () => {
const response = await client.get(`... api endpoint`);
return response;
};
const store = useStore();
// versionControl is a dummy variable added for testing to make sure the component is updated;
// it is updated: I tried adding console.log to my component function (where I have const client = useProfile(clientId)...)
const [versionControl,setVersionControl] = useState(0);
const updateVersion = () => setVersionControl(versionControl+1);
// TODO: useSelector not working
const updateData = newVal => { dispatch(actionUpdateData({id:userId,data:newVal})); updateVersion(); };
const dispatchPendStart = newVal => { dispatch(actionDispatchPendStart({id:userId})); updateVersion(); };
const dispatchError = newVal => { dispatch(actionDispatchError({id:userId,error:newVal})); updateVersion(); };
const [
getDataFromStoreGetter,
getLoadingStateFromStoreGetter,
getLoadingErrorFromStoreGetter,
] = [
() => (store.getState().profile.entities[userId]||{}).data,
() => (store.getState().profile.entities[userId]||{}).state,
() => (store.getState().profile.entities[userId]||{}).error,
];
const [
dataFromUseSelector,
loadingStateFromUseSelector,
loadingErrorFromUseSelector,
] = [
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].data : undefined ),
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' ),
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingError : undefined ),
];
useEffect( async () => {
if( !(['pending','succeeded','failed'].includes(getLoadingStateFromStoreGetter())) ) {
// if(requestOverflowCounter>100) { // TODO: protect against infinite loop of calls
dispatchPendStart();
try {
const result = await actionFunction();
updateData(result);
} catch(e) {
dispatchError(e);
throw e;
}
}
})
return {
versionControl, // "versionControl" is an approach to force component to update;
// it is updating, I added console.log to the component function and it runs, but the values
// from useSelector are the same all the time, never updated; the problem is somewhere else; useSelector is just not working
// get data() { return getDataFromStoreGetter(); }, // TODO: useSelector not working; but I need subscribtions
// get loadingState() { return getLoadingStateFromStoreGetter(); },
// get loadingError() { return getLoadingErrorFromStoreGetter(); },
data: dataFromUseSelector,
loadingState: loadingStateFromUseSelector,
loadingError: loadingErrorFromUseSelector,
};
}
export default useProfile;
store.js
import { configureStore,combineReducers } from '#reduxjs/toolkit';
import profileReducer from '../features/profile/profile-slice';
// import other reducers
export default configureStore({
reducer: {
profile: profileReducer,
// ... other reducers
},
});
component.js - actually see the use pattern above, there's nothing interesting besides the lines posted.
So
When I export loading state (I mean last lines in use-profile.js; I can suppress last three lines and uncomment the other three). So, if I use getLoadingStateFromStoreGetter (values retrieved via store.getState()...), then some profile names are displaying names that were fetched and some are holding "loading..." and are stuck forever. It makes sense. The correct data is retrieved from redux store and we have no subscribtions.
When I export the other version, created with useSelector, I always get its initial state. I never receive any user name or the value indicating "loading".
I have read many answers on StackOverflow. Some common mistakes include:
Some are saying your component is not getting updated. It's not the case, I tested it placing console.log to the code and adding the versionControl variable (see in the code) to make sure it updates.
Some answers are saying you don't update the store with reducers correctly and it still holds the same object. It's not the case, I tried both approaches, to return a fresh new object {...state,entities:{...state.entities...etc...}} and mutating the existing proxy object - both way my reducers should provide a new object and redux should notify changes.
Sometimes multiple store instances are created and things are messed. It's definitely not the case, I have a single call to configureStore() and a single component.
Also I don't see hook rules violation in my code. I have an if statement inside the useSelector fn but the useSelector hook itself is called unconditionally.
I have no idea what other reasons are causing useSelect to simply not work. Could anyone help me understand?
Ops, as usual, very simple typo is the reason. So many hours spent. Very sorry to those who have spent time trying to look at this and thanks for your time.
useSelector( state => !!state.profile.entities[userId] ? state.profile.entities[userId].loadingState : 'idle' )
There should be not .loadingState but .state. That's it.
My app is originally loading data from local-storage, Now i am trying to use firebase.
Firebase always tend to return a promise. So I am trying to convert the store into firebase return one.
here is the original one
export const loadState = () => {
const state: AppState = getDefaultState();
VALID_LABS.forEach(labId => {
state.labs[labId] = getDefaultLabState();
STORAGE_CONFIG.forEach(storageField => {
const { statePath, storageKey, defaultValueFn } = storageField;
const loadedValue = getFromLocalStorage(
labId,
storageKey,
defaultValueFn()
);
cachedValues[`${labId}:${storageKey}`] = loadedValue;
set(state, `labs.${labId}.${statePath}`, loadedValue);
});
});
return state as AppState;
};
const store = createStore(rootReducer, loadState(), applyMiddleware(thunk));
store.subscribe(
throttle(() => {
saveState(store.getState());
}, 500)
);
as you can see i am making process working fine. But the problem arise when i start using firebase.
my loadState become something like this.
export const loadState = (): AppState => {
if (firebase.auth().currentUser.uid) {
let userId = firebase.auth().currentUser.uid;
return firebase
.database()
.ref('/users/' + userId)
.once('value')
.then(function(snapshot) {
return snapshot.val() as AppState;
}).catch((err) => {
console.error(err)
})
}
};
So I also need to convert the store to accept the promise returning from the new loadState.
I don't know how to convert it since I am also using applyMiddleWare(thunk)
let saveState: (state: AppState) => void
 ;
let loadState: () => AppState;
if(firebase.auth().currentUser){
loadState = loadStateFirebase;
saveState = saveStateFirebase;
}else{
loadState = loadStateLocalStorage;
saveState = saveStateLocalStorage;
}
// call loadstate then data,pass it in as second para to appstate store
const store = createStore(rootReducer, loadState(), applyMiddleware(thunk));
store.subscribe(
throttle(() => {
saveState(store.getState());
}, 500)
);
Can someone help me
First things first, there is a small bug, you should probably throw err in the end of your catch block, if you don't, error will be considered handled and whatever is returned from catch will be considered the value of the promise. In this case it is undefined, so if an error happens, loadState will resolve to undefined instead of AppState object, which can cause some issues, but that's up to you, maybe you have other plans how to handle this
Anyway, the problem with promises is that as soon as one single function starts using it, everything else must as well. The easier option would be to create store in an asynchronous function:
function createStore() {
return loadState()
.then(state => {
const store = createStore(rootReducer, state, applyMiddleware(thunk))
// Either subscribe to store here
return store
})
}
const storePromise = createStore()
// or here
/* storePromise.then(store => store.subscribe(...))
or using async/await
async function createStore() {
const state = await loadState()
return createStore(rootReducer, state, applyMiddleware(thunk))
}
const storePromise = createStore()
It's hard to tell if this will suit your application, judging by tags, you're using react, so you may need to add extra logic to load this store for example write a StoreProvider and wrap your App with it:
function StoreProvider({ children }: { children?: ReactNode }) {
const [store, setStore] = useState<Store<AppState> | null>(null)
useEffect(() => {
createStore().then(setStore)
}, [])
if(!store) return <Loading /> // or whatever
return <Provider store={store}>{children}</Provider>
}
This may be fine for you or may not. Usually this is not very good, because your app cannot render, while firebase is loading its stuff. Another solution would be to initialize store with empty state in the beginning, render the parts of your application that don't need firebase staff and let others wait for it to load, you could use thunks for this or create three actions LOAD_STORE/REQUEST, LOAD_STORE/SUCCESS, LOAD_STORE/ERROR and make your app render in the appropriate way. You could maybe create a system that would queue some actions that need store while it is loading and executes them automatically as soon as it finished, there is a lot of space for creativity and frustration here, designing asynchronous stores is quite a challenge and you need to decide how you want to do it depending on your application needs
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
I have a component called PartyDetails, which needs data fetched by an ajax call. I want to show a Loading component while the ajax request is in progress.
The problem is that in order to determine whether the data is loaded or not, I need access to the store. This is how my enhance looks like:
const enhance = compose(
propSetter,
lifecycleEnhancer,
loading,
)
export default enhance(PartyDetails)
where propSetter is:
const propSetter = connect((state) => {
const { party } = state
const { dataLoaded } = party
// for some reason state does not contain match, and I'm resorting to routing
const { routing: {location: { pathname }}} = state
const involvedPartyId = pathname.substring(pathname.lastIndexOf('/') + 1)
return { dataLoaded, involvedPartyId }
}, {loadParty})
and lifecycleEnhancer is:
const lifecycleEnhancer = lifecycle({
componentDidMount() {
this.props.loadParty(this.props.involvedPartyId)
}
})
and loading is ( notice that in this case, dataLoaded comes from the previous connect that has been done in propSetter ):
const loading = branch(
({dataLoaded}) => dataLoaded,
renderComponent(connect(mapStateToProps, mapDispatchToProps)(PartyDetails)),
renderComponent(Loading)
)
So basically, if the data has been fetched, I am using a 2nd connect to obtain the relevant props for PartyDetails.
I just started learning recompose a few days ago, and I could not find an example that fitted my use case. The above is what I came up with after reading through the docs, and some examples found in other articles.
Is what I'm doing a good way of handling this? Could this be done in a better way, maybe without needing 2 connect calls?
You could write all your logic for mapping state and dispatch to props in one connect:
export default compose(
connect(
state => {
const { party } = state;
const { dataLoaded } = party;
const { routing: { location: { pathname } } } = state;
const involvedPartyId = pathname.substring(pathname.lastIndexOf("/") + 1);
// also put here your `mapStateToProps` code
return { dataLoaded, involvedPartyId };
},
{
loadParty
// the same here, append `mapDispatchToProps` logic
}
),
lifecycle({
componentDidMount() {
this.props.loadParty(this.props.involvedPartyId);
}
}),
branch(
({ dataLoaded }) => dataLoaded,
renderComponent(PartyDetails),
renderComponent(Loading)
)
// as `branch` is returning HOC, we need to provide empty component to it in
// order to render whole component
)(createSink());