I have a multi-step form application, and I'm struggling with getting my head around how I can save my redux state and replay it after a refresh for example? Going back/forward in the app works as expected, but after a browser refresh, my previous state is empty. Ideally I'd like to be able to save prior state in session storage relating to a path, so that I can replay later, but I'm not seeing how I can do that easily. Has anyone done anything like this and can provide some pointers? Thanks.
It looks like you're trying to use a single-page app framework within a multiple-page context. To make the two play nicer together, you could look into making your own middleware that synchronizes state to and from localStorage to create an app that appears to not have lost any state after a refresh/page navigation.
Similar to the way that redux-logger logs both the previous and next states, I'd probably start by plugging in a middleware at the beginning (localStorageLoad) and end (localStorageDump) of the createStoreWithMiddleware function creation (right before redux-logger):
// store/configureStore.js
const createStoreWithMiddleware = applyMiddleware(
localStorageLoad, thunk, promise, localStorageDump, logger
)(createStore);
Then fire an initial action right when you initialize your app to load stored state before your app renders:
// index.js
const store = configureStore();
store.dispatch({ type: 'INIT' });
ReactDOM.render(<App />, document.getElementById('root'));
The localStorageLoad would handle the INIT action and dispatch some sort of SET_STATE action, which would contain a payload with the state that was previously saved in localStorage.
// middleware/localStorageLoad.js
export default store => next => action => {
const { type } = action;
if (type === 'INIT') {
try {
const storedState = JSON.parse(
localStorage.getItem('YOUR_APP_NAME')
);
if (storedState) {
store.dispatch({
type: 'RESET_STATE',
payload: storedState
});
}
return;
} catch (e) {
// Unable to load or parse stored state, proceed as usual
}
}
next(action);
}
Then, add a reducer which replaces the state with that payload, effectively rebooting the app as it was previously.
To complete the loop, you'd need a localStorageDump middleware that comes at the end of the chain that saves each reduced state object into localStorage. Something like this:
// middleware/localStorageDump.js
export default store => next => action => {
const state = store.getState();
localStorage.setItem('YOUR_APP_NAME', JSON.stringify(state));
next(action);
}
Just an idea, haven't actually tried it. Hope that helps get you started towards a solution.
You can't store state on refresh this is normal. Typically you store these inside cookies or web storage.
Cookies are synced between browser, server and are set on every request you perform.
Localstorage gives you up to 5MB to store data in and never gets send with the request.
Redux:
Set localstorage properties when filling in form. (be sure to clean it when complete)
Refresh the page with an incomplete form.
When react mounts your Form component, use componentWillMount method to read the localstorage and dispatch the data to a reducer
The reducer updates the redux-state which in turn updates the component with the properties you got from localstorage.
If you use redux to store form data dispatch the properties of localstorage.
If not read the localstorage inside the Form component and set the initial values.
Hope this helps !
Related
Using Redux, is it true that any component and sub-component on the page get all data of the one and only store, and be able to send out any action at all, even if it is not meant for that component to send out?
Can all the components use
const store = createStore(mainReducer);
let state = store.getState();
and be able to see all states of the whole app? Can any component dispatch any action all all? So for example, if there is Counter component and a Comment component, can the Comment component accidentally send out a "INCREASE_COUNT" action?
Using Redux any component "can" access any data in the store, but for the access you have to 'connect' it with the store. When you 'connect' you also specify a map to which part you want this component to access. That's how you are in control, it only gets access to what you want only.
The same goes for actions. You have to map the actions also - which component can dispatch which action, when your 'connect' to the store.
Check this out for more info - https://redux.js.org/basics/usage-with-react
To most part of your question, it seems the answer is Yes.
Yes, the components can access the whole store ( one it has subscribed to ) and can dispatch actions when needed. I do not think there is any way you can put action/store behind some restrictions.
can the Comment component accidentally send out an "INCREASE_COUNT" action? Yes if you try to dispatch it again from the child component.
If you could add any specific example you have to ask, I can add more to my answer.
I hope it helps you !
" every component has access to the store" is wrong, it is like this " every component has access to the state and actions in the store that you "the developer" specify.
for a component to able to access the store, you need to wrap it in the connection function like so
import { connect } from "react-redux";
// Your component
export default connect(mapStateToProps, dispatchActionToProps);
// the component will only have access to the store props and actions that you specify
// in mapStateToProps and dispatchActionToProps
const mapStateToProps = state => {
return {
// state is the result of combineReducers
// whatevery key the component needs you can specify here
};
}
const dispatchActionToProps = dispatch => {
return {
// your store actions
};
}
My questions is a conceptual one and based on the issue outlined in this post: React Redux capture updated store state after updating database. I don't think any code is needed to understand or be able to answer it. But if not it is at the link above.
I think I might have missed a small detail about the react/redux state update process following an action that changes the back-end data that a state variable reflects. My question is: When I dispatch a save action, should I then also be dispatching a request to update any state that depends on that underlying data?
So for example, right now the way I'm thinking about it and implementing my code is as follows:
app starts and ParentComponent loads and dispatches GET_DATA on componentDidMount which initializes state variable data which is reflected on ParentComponent in a table
when a link is clicked on ParentComponent, ParentComponent renders ChildComponent which is a react-modal popup that displays elements of data so it can be updated
there is and Save and Close button on ChildComponent; when you click the button, SAVE_DATA is dispatched and the changes to data that are made on ChildComponent get saved to the database
THIS is where my question arises... at this point should I also be calling GET_DATA to dispatch the process of "refreshing" data in my state? Would this be the right way to handle saving data to a database when using redux so that all components that rely on data get updated?
Note: What I'm currently doing is that after step 3, I am simply triggering a refresh function in ParentComponent so that it rerenders and hence reflects data in state. The epiphany I just had is that there is no way for data in state to reflect the new saved data because GET_DATA has not been dispatched after saving and rerendering the component does not trigger GET_DATA.
Are my assumptions correct? Should I be calling GET_DATA somewhere else in my ParentComponent like ComponentWillReceiveProps? The issue I had here is that maybe I'm doing something wrong, but it triggers an endless loop. Somehow though I feel that is the only place where I can address my need to dispatch GET_DATA after the local ParentComponent state is changed by setting refresh (a ParentComponent state variable) to true.
I think it would benefit you to refactor your actions a bit to take advantage of the action/middleware/reducer pattern.
You would have an action GET_TRANSACTIONS, that would take your year param. Your transactionsMiddleware would respond to the GET_TRANSACTIONS action by making your fetch request and would dispatch GET_TRANSACTIONS_SUCCESS with the respond data on success. You transactions reducer would then process the data into your store.
actions
export const getTransactions = year => {
return {
type: "GET_TRANSACTIONS",
year
};
};
export const getTransactionsSuccess = payload => {
return {
type: "GET_TRANSACTIONS_SUCCESS",
payload
};
};
middleware
function getTransactions(year) {
fetch().then(response => dispatch(actions.getTransactionsSuccess(response.data));
}
reducer
const getTransactionsSuccess = (state, action) => {
return Object.assign({}, state, newStuffFromActionPayload);
}
You would also have an action SAVE_TRANSACTIONS, which would be what your button would dispatch, along with the data to save. Your transactionsMiddleware would respond to the action by dispatching the update request. Your API would return the data from the updated record.
This is where you would have the middleware dispatch a follow-up action. It could be your getTransactions action, but it'd be even better to dispatch an action that your reducer would respond to by merging in the new data to your store.
actions
export const updateTransaction = payload => {
return {
type: "UPDATE_TRANSACTION",
payload
};
};
export const updateTransactionSuccess = payload => {
return {
type: "UPDATE_TRANSACTION_SUCCESS",
payload
};
};
middleware
function updateTransaction(transUpdate) {
fetch().then(response => dispatch(actions.updateTransactionSuccess(response.data))
}
reducer
const updateTransactionSuccess = (state, action) => {
find the record in the state, update it with data from action.payload
return Object.assign({}, state, updatedRecord);
}
If everything is set up correctly, it should trigger an update on your parent when it detects the change in the store. You avoid making two API calls for every save as well.
Following code using simple Redux API:
// Dummy Reducer that just returns previous state
const counter = (state = 0, action) => {
return state;
}
// Store
const { createStore } = Redux;
const store = createStore(counter);
const listener = () => {
console.log('Listener called...with ' + store.getState());
};
// Listener
store.subscribe(listener);
// Manually dispatching actions
store.dispatch({ type: 'DUMMY' });
store.dispatch({ type: 'DUMMY' });
store.dispatch({ type: 'DUMMY' });
produces following output:
Listener called...with 0
Listener called...with 0
Listener called...with 0
My question:
If nothing changes in the store, why listener is being notified as if something changed. Isnt is unnecessary and counter productive? Lets say, the listeners are views like React Container Components. They will try rerendering unnecessarily right ?
Or am I missing something?
In case of Flux, I feel we have higher flexibility in terms of whether to publish the change from the store or not. Is this a con for Redux over Flux ? Or am I missing something?
According to the docs, store.subscribe():
Adds a change listener. It will be called any time an action is dispatched, and some part of the state tree may potentially have changed.
Many (most?) popular redux patterns do not require developers to use store.subscribe() at all. See react-redux and redux-saga.
However, it's a good question whether store.subscribe affects performance of such frameworks.
With react-redux, the most popular redux framework for react, container components don't do long-running tasks like http requests; those are typically handled by dispatched actions. So container components tend to be very high-performance, simply pulling data out of simple objects in the store. When their output doesn't change, then the associated view components won't re-render.
I have an action that sends a Firebase query that relies on store data - specifically the uid.
The uid IS populated and stored upon user sign-in, which is stored in an auth object.
In this component, I'm retrieving the uid, firing the action, receiving the payload, updating the state with a reducer, and rendering the component with the data.
I've mapped the state to props thus (initially this.props.uid is always undefined):
function mapStateToProps(state, props) {
return {
uid: state.auth.uid,
componentData: state.thisComponent.componentData
}
}
I check the uid thus (written before render()):
componentWillReceiveProps(nextProps) {
if (nextProps.uid !== this.props.uid) {
this.props.actions.getComponentData(nextProps.uid)
}
}
The action and query work, but only if the component is refreshed - not if the component is simply navigated to. Additionally, I'm using redux-persist to hydrate the store and persist its state:
**index.js**
... combineReducers etc
let store = createStore(rootReducer, applyMiddleware(thunk), autoRehydrate());
persistStore(store)
How do I make sure that componentWillReceiveProps always dispatches the action and the payload is rendered - whether the page is opened from a different link or when it is refreshed?
componentWillReceiveProps is not run when the component is mounted, it is only run, like you say, after initialization and when the component will receive a new set of props.
I believe you are looking for componentWillMount, anything put in here will run before the component is mounted, meaning, this life-cycle event is invoked immediately before the render method.
componentWillMount(props) {
props.actions.getComponentData(props.uid)
}
Take a look at http://busypeoples.github.io/post/react-component-lifecycle/ for more information
Depending on your use case you might need it on both.
I am using react-router and redux in my latest app and I'm facing a couple of issues relating to state changes required based on the current url params and queries.
Basically I have a component that needs to update it's state every time the url changes. State is being passed in through props by redux with the decorator like so
#connect(state => ({
campaigngroups: state.jobresults.campaigngroups,
error: state.jobresults.error,
loading: state.jobresults.loading
}))
At the moment I am using the componentWillReceiveProps lifecycle method to respond to the url changes coming from react-router since react-router will pass new props to the handler when the url changes in this.props.params and this.props.query - the main issue with this approach is that I am firing an action in this method to update the state - which then goes and passes new props the component which will trigger the same lifecycle method again - so basically creating an endless loop, currently I am setting a state variable to stop this from happening.
componentWillReceiveProps(nextProps) {
if (this.state.shouldupdate) {
let { slug } = nextProps.params;
let { citizenships, discipline, workright, location } = nextProps.query;
const params = { slug, discipline, workright, location };
let filters = this._getFilters(params);
// set the state accroding to the filters in the url
this._setState(params);
// trigger the action to refill the stores
this.actions.loadCampaignGroups(filters);
}
}
Is there a standard approach to trigger actions base on route transitions OR can I have the state of the store directly connected to the state of the component instead of passing it in through props? I have tried to use willTransitionTo static method but I don't have access to the this.props.dispatch there.
Alright I eventually found an answer on the redux's github page so will post it here. Hope it saves somebody some pain.
#deowk There are two parts to this problem, I'd say. The first is that componentWillReceiveProps() is not an ideal way for responding to state changes — mostly because it forces you to think imperatively, instead of reactively like we do with Redux. The solution is to store your current router information (location, params, query) inside your store. Then all your state is in the same place, and you can subscribe to it using the same Redux API as the rest of your data.
The trick is to create an action type that fires whenever the router location changes. This is easy in the upcoming 1.0 version of React Router:
// routeLocationDidUpdate() is an action creator
// Only call it from here, nowhere else
BrowserHistory.listen(location => dispatch(routeLocationDidUpdate(location)));
Now your store state will always be in sync with the router state. That fixes the need to manually react to query param changes and setState() in your component above — just use Redux's Connector.
<Connector select={state => ({ filter: getFilters(store.router.params) })} />
The second part of the problem is you need a way to react to Redux state changes outside of the view layer, say to fire an action in response to a route change. You can continue to use componentWillReceiveProps for simple cases like the one you describe, if you wish.
For anything more complicated, though, I recommending using RxJS if you're open to it. This is exactly what observables are designed for — reactive data flow.
To do this in Redux, first create an observable sequence of store states. You can do this using rx's observableFromStore().
EDIT AS SUGGESTED BY CNP
import { Observable } from 'rx'
function observableFromStore(store) {
return Observable.create(observer =>
store.subscribe(() => observer.onNext(store.getState()))
)
}
Then it's just a matter of using observable operators to subscribe to specific state changes. Here's an example of re-directing from a login page after a successful login:
const didLogin$ = state$
.distinctUntilChanged(state => !state.loggedIn && state.router.path === '/login')
.filter(state => state.loggedIn && state.router.path === '/login');
didLogin$.subscribe({
router.transitionTo('/success');
});
This implementation is much simpler than the same functionality using imperative patterns like componentDidReceiveProps().
As mentioned before, the solution has two parts:
1) Link the routing information to the state
For that, all you have to do is to setup react-router-redux. Follow the instructions and you'll be fine.
After everything is set, you should have a routing state, like this:
2) Observe routing changes and trigger your actions
Somewhere in your code you should have something like this now:
// find this piece of code
export default function configureStore(initialState) {
// the logic for configuring your store goes here
let store = createStore(...);
// we need to bind the observer to the store <<here>>
}
What you want to do is to observe changes in the store, so you can dispatch actions when something changes.
As #deowk mentioned, you can use rx, or you can write your own observer:
reduxStoreObserver.js
var currentValue;
/**
* Observes changes in the Redux store and calls onChange when the state changes
* #param store The Redux store
* #param selector A function that should return what you are observing. Example: (state) => state.routing.locationBeforeTransitions;
* #param onChange A function called when the observable state changed. Params are store, previousValue and currentValue
*/
export default function observe(store, selector, onChange) {
if (!store) throw Error('\'store\' should be truthy');
if (!selector) throw Error('\'selector\' should be truthy');
store.subscribe(() => {
let previousValue = currentValue;
try {
currentValue = selector(store.getState());
}
catch(ex) {
// the selector could not get the value. Maybe because of a null reference. Let's assume undefined
currentValue = undefined;
}
if (previousValue !== currentValue) {
onChange(store, previousValue, currentValue);
}
});
}
Now, all you have to do is to use the reduxStoreObserver.js we just wrote to observe changes:
import observe from './reduxStoreObserver.js';
export default function configureStore(initialState) {
// the logic for configuring your store goes here
let store = createStore(...);
observe(store,
//if THIS changes, we the CALLBACK will be called
state => state.routing.locationBeforeTransitions.search,
(store, previousValue, currentValue) => console.log('Some property changed from ', previousValue, 'to', currentValue)
);
}
The above code makes our function to be called every time locationBeforeTransitions.search changes in the state (as a result of the user navigating). If you want, you can observe que query string and so forth.
If you want to trigger an action as a result of routing changes, all you have to do is store.dispatch(yourAction) inside the handler.