How is it possible to have both a preloadedState (hydrating from server) and inject reducers dynamically?
In react-boilerplate or How to dynamically load reducers for code splitting in a Redux application? there is the concept of reducers which are added dynamically, based on the page/components you are viewing. Extract from reducers.js:
export default function createReducer(asyncReducers) {
return combineReducers({
users,
posts,
...asyncReducers
});
}
While this works well when navigating from one page to another (or on a client-side only application); when hydrating data from the server I am encountering this error:
Unexpected property "comments" found in previous state received by the reducer. Expected to find one of the known reducer property names instead: "users", "posts". Unexpected properties will be ignored.
(where comments is the property name of the dynamically injected reducer)
The reason for this error is clear, since the preloadedState coming from the server (using React SSR) already contains comments and the initial reducer does not since this is added dynamically afterwards. The error disappears if I add comments to my combineReducers; however that means that at app initialisation I need to include all reducers; which is not ideal.
You should be able to use dummy reducers in place of dynamically loaded reducers which will be replaced when the real reducers are loaded.
{ comments: (state = null) => state }
This can also be done automatically, depending on your implementation, as per http://nicolasgallagher.com/redux-modules-and-code-splitting/
// Preserve initial state for not-yet-loaded reducers
const combine = (reducers) => {
const reducerNames = Object.keys(reducers);
Object.keys(initialState).forEach(item => {
if (reducerNames.indexOf(item) === -1) {
reducers[item] = (state = null) => state;
}
});
return combineReducers(reducers);
};
Related
I am digging into React with Redux for a rewrite of our product.
A lot of fog around Redux was cleared by using Redux-Toolkit https://redux-toolkit.js.org/.
Then I found that React-Router made state management messy and found a solution in redux-first-router https://github.com/faceyspacey/redux-first-router.
Now I want to combine these excellent libraries. But I think I'm doing something wrong in the configuration.
Here is the code. Starting with a sandbox example at https://codesandbox.io/s/m76zjj924j, I changed the configureStore.js file into (for simplicity I have omitted code for the user reducer)
import { connectRoutes } from 'redux-first-router';
import { configureStore, getDefaultMiddleware } from '#reduxjs/toolkit'
import { routePaths } from '../routes';
const { reducer: location } = connectRoutes(routePaths);
const {
middleware: routerMiddleware,
enhancer: routerEnhancer,
initialDispatch
} = connectRoutes(routePaths, { initialDispatch: false });
export default function configureRouteStore() {
const store = configureStore({
reducer: {
location: location
},
middleware: [...getDefaultMiddleware(), routerMiddleware],
enhancers: (defaultEnhancers) => [routerEnhancer, ...defaultEnhancers]
})
initialDispatch();
return store;
}
But now each time a change in route = Redux store is updated, I get an exception in the browser:
index.js:1 A non-serializable value was detected in the state, in the path: `location.routesMap.PROFILE.thunk`. Value: dispatch => {
dispatch(USER_DATA_LOADED({
avatar: null
}));
const avatar = `https://api.adorable.io/avatars/${Math.random()}`;
setTimeout(() => {
// fake async call
dispatch(USER_…
Take a look at the reducer(s) handling this action type: HOME.
I can see that this stems from the routes definitions if the route has a 'thunk' property defined as this: PROFILE: { path: "/profile/:username", thunk: fetchUserData },
If I change the thunk property to a serializable value (or remove it) the error is gone.
Somehow now the thunk is added to the payload of the action to update paths. What...?
What to do? OK, I can get it work with the traditional Redux setup but as I am a big fan the redux toolkit it would be sweet for me and maybe a few more people out there to make it work with the toolbox.
I'm a Redux maintainer and creator of Redux Toolkit.
Based on that error message and reading the Redux-First-Router source code, it looks like the library is indeed attempting to store thunk functions in the Redux store. This is a problem, because we specifically instruct users to never put non-serializable values like functions in state or actions.
By default, Redux Toolkit adds a "serializable state invariant middleware" that warns you if non-serializable values are detected in state or actions, to help you avoid accidentally making this mistake.
It is possible to pass some options to getDefaultMiddleware() to customize the behavior of these middlewares. There is currently an ignoredActions option, but I don't think we have an option to ignore specific sections of the state tree. The included redux-immutable-state-invariant middleware does have an ignore option for portions of the state, so perhaps we could add that approach.
I've added https://github.com/reduxjs/redux-toolkit/issues/319 to see if we can add an option like that.
In the meantime, you could potentially turn off the middleware by calling getDefaultMiddleware({serializableCheck: false}).
update
I've just published Redux Toolkit v1.2.3, which adds an ignoredPaths option to the serializability check middleware to allow ignoring specific keypaths within the state.
Again, please note that this is purely an escape hatch to work around misbehaving libraries, and should not be used as a regular approach.
I have a api call which takes in a varable from the state in my actions which goes to an axios get and it passes it there. I know this because if I console the variable in the axios get its there which goes to a route and to the controller but when I console loge the request in the controller its empty. I am trying to do a find() to a specific email. If I hard code it then it works perfectly which means my variable probably isn't passing there and I don't know why. I have a post that works perfectly
my action
export const getUser = (currentUser) => {
return(dispatch, getState) => {
API.getUserInfo({
emailaddress:currentUser.emailaddress,
password: currentUser.password
})
.then(res => {
dispatch({type:"USER_PROFILE",userPro:res.data})
})
.catch(err => console.log(err));
}
}
reducer
const initState ={
userProfile:[]
}
const userReducer = (state = initState,action)=>{
switch(action.type){
case "CREATE_USER" :
console.log("created User", action.newProfile)
return state;
case "USER_PROFILE":
console.log("User", action.userPro)
return {
userProfile: [...state.userProfile,action.userPro]
}
default:
return state;
}
}
root reducer
const rootReducer = combineReducers({
authR: authReducer,
userR:userReducer
})
mapstatetoprops
const mapStateToProps = (state)=>{
console.log(state)
return{
userInfo:state.userR.userProfile
}
}
export default connect( mapStateToProps ) (Layout);
Right, so you're now moving away from component state and into application state. This might end up being a long answer but in short I would suggest you read-up on the Redux and React-Redux documentation. https://redux.js.org/ https://react-redux.js.org/
Redux is all about persisting data in your application. Which is what you need if you want to take the data from one page and make it available for use in another page. It is essentially comprised of three parts:
Actions: Functions that are called to carry data from your components or APIs into a common place (reducers). The data is recognized as a "payload."
Reducers: Functions that are triggered by your actions. They use the "payload" to return a new app state (data).
Store: Just like the name suggests, it is an object that contains all your reducers; a collection of all your application states.
Now react-redux is simply middleware that let's your components communicate with your store.
There is some pretty standard mark-up to get this all to work. I'll show you examples with what I assume your code looks like.
So first let's define a reducer (a data maintainer for lack of better words) and lets store it in a file called authReducer.js
const authReducer = (state = {}, action) => {
switch(action.type){
CASE "SET_USER_CREDS":
return {
user: action.payload
}
default:
return state
}
}
export default authReducer
So digging into this code. We defined a function with two parameters, a state which we gave an initial value of {} and an action, which refers to the actions that get sent to this reducer. If there was an action with a type of "SET_USER_CREDS" then the reducer returns an object that will contain information on a user. As we can see, the only way it can get data is by consuming it from an action.
Now we need an action, a means to communicate with the reducer we just made. Let's create a file called authActions.js
export const recordUser = (userData) => {
return {
type: "SET_USER_CREDS":
payload: userData
}
}
Looks simple enough, we created a function that essentially is trying to meet the requirements of making our reducer to work. These action creators are actually used by our components, this is method in which we can get data from a component and keep it somewhere.
But wait, where do we keep this data? We talked about reducers, but where do they live? Well it's time to build our store.
store.js
import {createStore, combineReducers} from "redux"
import authReducer from "./authReducer"
const store = createStore(combineReducers({
auth: authReducer
}))
export default store
Alright we got a store now. Quick facts about the syntax. We used a few methods fromt the redux library. We used createStore() and we passed in combineReducers(), where the latter accepts an object. In the object we define a key-value pair for each reducer we want to put in our store. The key is typically the name/type of data the reducer is managing.
Cool, we've set up a store, a reducer and an action-creator. But as is, there is no way for React to communicate with your store. Well this is where react-redux comes in. In whereever you defined your react-router-dom routes, we'll need to make some modifications.
Let's just say this is your router file, you'll need to add the following.
import {Provider} from "react-redux"
import store from "./store"
<Provider store={store}>
//Routes
</Provider>
<Provider> is a component that accepts a redux-store as an argument. We can wrap our <Routes/> inside of it, thus providing the store to all our components.
Congrats, we're about 2 steps away from getting all this to work.
So now in your Header component, or wherever you're entering the user data you need to do a few things.
1) Bring in some dependencies.
import {connect} from "react-redux"
import {recordUser} from "./authActions"
2) Outside of your component. Define a function called mapDispatchToProps()
const mapDispatchToProps = (dispatch) => {
return {
recordUser: (userData) => {
dispatch(recordUser(userData))
}
}
}
In short, this is a function that will let us call your action-creators inside your component. The recordUser key is now an available prop inside your component.
3) Inside your component you need to define an event-handler to use our new prop. It will need to be triggered when the user is navigating to the other page.
handleRecordUser = () => {
const userData = {
email: this.state.email,
password: this.state.password
}
this.props.recordUser(userData)
}
So its doing as we promised, taking data from our component state and passing it off to an action creator. Remember, you need to call this event-handler to execute this action at the same time as the re-route to the new page. If you're using a <Link> to reroute, just do something like:
4) Modify the export of this component to use connect()
export default connect(null, mapDispatchToProps)(Header)
connect() gives our component access to methods like dispatch() which is a requirement to use your action-creators.
Last, but not least, consume your store.
In the component you routed to you need to do the following:
1) Bring in some dependencies.
import {connect} from "react-redux"
2) Outside your component, define a function called mapStateToProps()
const mapStateToProps = (state) => {
return {
auth: state.auth
}
}
mapStateToProps() let's you tap into the store state and enables you to choose which reducer data you want to bring into your component. state.auth should look familiar, since in our store.js file we defined a key-value pair of {auth: authReducer}. We're simply calling that key.
So by defining a key of auth, I'm now saying that I will have a prop in my component called this.props.auth and it's value will be the reducer ouput.
3) Lastly, connect your component
export default connect(mapStateToProps)(YourComponent)
Now you can utilize the data from your previous Login component by consuming the saved data in your redux store by making use of this.props.auth.
you can use redux over here or you can use the localstorage, cookie, sessions any one of these browser storage to set the values and when your component gets rendered you can retrieve this data from browser and make your API call. Though this is not the best approach but if you don't know how to use redux then you can apply this.
I have a component which is a form which I use to create records in my database. I also want to be able to pass into this component values with which to populate the form, allowing me to then update my existing database records. Straightforward add/edit functionality from the same component.
The following code should explain how I am doing this. The media prop is an object containing the data. I have this data already in the parent element so setting the values here is fine and they pass thru without problem. However once the page is loaded the 3rd init argument of useReducer never re-triggers, and therefore my state cannot be overridden with the values passed down in the media prop. Is there a correct way to make the init function trigger when the props are updated, or is my issue architectural?
const MediaUploadForm = ({ media }) => {
const init = (initialState) => {
if (media) {
// ... here I extract the values I need and override the initialState where required
} else {
return initialState
}
}
const [formState, dispatch] = useReducer(
MediaFormReducer,
initialState,
init
)
So using the new React hooks features and keeping the component functional allows me to use useEffects() This is similar to using a componentDidUpdate type event. So the following code allows me to check for the status of a prop (media) and then dispatch an action that sets my redux state.
useEffect(() => {
if (media && id !== media.id) {
dispatch(loadMedia(media))
}
})
Thanks to #sliptype for pointing me in the right direction
Copying props into state is considered an anti pattern in React. Props changes do not trigger reinitialising state, as you have seen.
This is described in https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html.
From the recap it looks like the current suggested solution matches
Alternative 1: To reset only certain state fields, watch for changes in a special property (e.g. props.userID).
This is an alternative, rather than the recommendation.
https://reactjs.org/blog/2018/06/07/you-probably-dont-need-derived-state.html#recap
Hope this link gives you more information around the topic, and the recommendations there help in future work.
I've a requirement to persist the redux form fields even after page reloads.
So my approach for this is
First before page refreshes all field values from my component will be saved to localstorage in componentDidUpdate method
componentDidUpdate(prevProps, prevState) {
prevProps.fields ? localStorage.setItem('fields', JSON.stringify(prevProps.fields)):null
prevProps.fields ? localStorage.setItem('fields', JSON.stringify(prevProps.fields)):null
}
Then after page refreshes, I'm checking if there are any fields are available in localstorage then I'm assigning those fields object directly to my reduxform initial values
let reduxFormFunc = reduxForm(
{
form: 'rentSelection',
destroyOnUnmount: false,
fields: guestEntryFields,
validate,
initialValues: getInitFields()
},
selectProps
)
function getInitFields() {
let initValues = {pgu: defaultpgu}
if(localStorage.getItem('fields') && JSON.parse(localStorage.getItem('fields')).listings.length>0){
console.log('selecting and assigning fields from localstorage')
let fields = JSON.parse(localStorage.getItem('fields'))
console.log(fields)
initValues['commonFields'] = fields.commonFields
initValues['listings'] = fields.listings
initValues['pgu'] = fields.pgu
}
console.log(initValues)
return initValues
}
export default reduxFormFunc(GuestEntry)
Now I can access "listings", "pgu", "commonFields" values in my GuestEntry react component. and listings object contains nested level of objects. While iterating and accessing it, all its "value" attribute String values of nested objects inside listings object are converted into object
From this discussion
https://github.com/erikras/redux-form/issues/576
I got to know that we have to call the value property on value attribute, something like this
listings[0].entry.value.value
here value is a string, but as its converted into object I need to call value.value.
So any quick solution to fix this issue?
Or is it fixed in latest version of redux-form? I'm currently using
"redux-form": "^4.2.0",
I didn't find any alternate way in this version, So I just updated the code in such a way that, If I get the value as a instance of object then I'm responding back with the value using "object.value", else I'm just simply returning the value.
I'm not sure if this is best practice but I'm simple using redux-localstorage to persist the redux-form values field to localstorage. This means that the form state object will remain as is in terms of validation rules and registered fields etc, but the values will persist (this is important because I don't want the validation to tell me that fields are invalid before they've been touched).
I wrote a custom slicer in the config object of redux-localstorage so that it knows which part of the form state to persist. Here's what my store.js file looks like as an example (I'm using redux-thunk in this particular application which you might not):
import { createStore, compose, applyMiddleware } from 'redux';
import thunk from 'redux-thunk';
import persistState from 'redux-localstorage';
import rootReducer from './reducers/root';
const enhancer = compose(
applyMiddleware(thunk),
persistState(['someOtherStateKey', 'form'], {
slicer: paths => state => {
let persist = {};
paths.forEach(path => {
switch (path) {
case 'form':
persist[path] = {
myFormName: {
values: state[path].myFormName.values,
},
};
break;
default:
persist[path] = state[path];
}
});
return persist;
},
})
);
function configureStore() {
return createStore(
rootReducer,
window.__REDUX_DEVTOOLS_EXTENSION__ &&
window.__REDUX_DEVTOOLS_EXTENSION__(),
enhancer
);
}
const store = configureStore(); // I instantiate here so I can `import` in utils files
export default store;
My Form.js would have the following code in it:
Form = reduxForm({
form: 'myFormName',
})(Form);
You don't need to worry about manually updating or retrieving from localstorage in this example, and you don't need component mount or unmount listeners.
See the redux-localstorage config.slicer docs for more information.
EDIT: I've decided to go with setting the initialValues after all as I wanted to combine a method of having default values in the form as well as persisting the form data. In my previous solution I noticed that using initialValues alongside persisting the redux-form values would cause issues. However, copying the form data to a separate key in the store, as well as using initialValues seemed to do the trick.
To do this, I'm setting the initialValues during initialisation of Redux Form to a combination of my default form state (held in the redux store) and the persisted form values (which are also held in store). I'm also making use of the onChange handler to execute my action creator, which stores the form values in a separate section of the store, and redux-localstorage persists this. Code as follows:
// initialState.js
export default {
values: {}
initialValues: {
agreeToTerms: false,
},
};
// Form.js
Form = connect(state => ({
initialValues: {
...state.initialValues, // our defaults
...state.values, // our persisted values
},
onChange: (values, dispatch, props) => {
props.myReduxActionFile.updateValues(values);
},
}))(Form);
// myReduxActionFile.js
...code to dispatch action to update store.values
// store.js
const enhancer = compose(
applyMiddleware(thunk),
persistState('values')
);
In my application i have many part of the state that is significative only when the user is logged in.
When the user is logged in and navigate directly to a page, router display the page but have to make async call, so a piece of the state is not yet populated.
Now, suppose i have to show a button on navigation bar that have to take a part of the state, but this state is not populated since the async call finis.
More pratically, i have this selector:
export const getSelectedCustomer = state => state.systems.selectedCustomer;
With this, there are no problem, because selectedCustomer is part of the initialState reducer
The problem is in this selector:
export const getSelectedCustomerName = state => {
return state.systems.entities[getSelectedCustomer(state)].customer;
}
The part of the state entities in initialState is an empty array and will be populated on async call.
So, when the application start and i map (with connect - mapStateToProps) the selector getSelectedCustomerName i get the error because entities is empty and sure, customer field do not exist yet
A simple solution if to take in place some if-else, but i will have to do this in "every" part of the application.
I'm asking if there is a better solution to avoid this...
const NullCustomerEntities = {
customer: "Anonymous"
}
export const getSelectedCustomer = state => state.systems.selectedCustomer;
export const getCustomerEntities = state => {
return state.systems.entities[getSelectedCustomer(state)] || NullCustomerEntities;
}
export const getSelectedCustomerName = state => {
return getCustomerEntities(state).customer;
}
This is a classic example of a Null Object pattern
(hope you don't mind the code being in Ruby).