Inter-SPA and intra-SPA post-load operations management in React Redux
Bài đăng này đã không được cập nhật trong 3 năm
Consider a scenario, where you're working on a SPA. You clicked a button, and in response of the click, you will be redirected to another SPA residing in the same origin or simply you want to reload the page (same SPA). For any of the two cases, if you want to perform some operation (like dispatching an action) immediately after load/reload of the SPA based on the last state of the SPA prior to the reload, there is no direct way to do that. Cause, any change you make to Redux state (or React state) will be lost as soon as you reload the SPA. So, performing an operation (after a reload) that depends on prior React or Redux state, will not be possible. In this article, we'll investigate an approach to address this issue.
Concept
Since React or Redux state doesn't survive a reload, a short term persistence layer can be utilized to address this issue. The layer must persist data during a reload. sessionStorage
is the perfect client for this task, cause it persists through an entire browser session, and different tab and window receives their different sets of sessionStorage
.
All the operations, that need to be performed after a load/reload are stored in the sessionStorage
in a structured way and gets executed on load/reload event.
A post-load operation lifecycle can be as simple as the steps below.
- A
PostLoadOperations
service is created - Operation types and execution strategies are specified in the service
- Operations are pushed to
sessionStorage
throughPostLoadOperations
service - When the target SPA gets loaded/reloaded, a batch processing is performed on accumulated operations (in the
sessionStorage
) - And, finally, the persistence layer gets flushed.
Usage
Let's walk through a demonstration project for a better understanding. You may grab the project from my github repo., which is a fully operational demo. and contains all the code explained here.
Post-load operations are managed through a service. The operational service takes an identical form like below.
# PostLoadOperations.js
import { PostLoadOperationType } from '../constants/Enums';
import { setPostLoadMessage } from '../actions/Home';
export default dispatch => {
const operations = JSON.parse(sessionStorage.getItem("postLoadOperations"));
return {
push(type, data) {
sessionStorage.setItem("postLoadOperations", JSON.stringify(operations ?
operations.concat({type, data}) : [{type, data}]));
},
run() {
if(operations) {
operations.forEach(operation => {
switch(operation.type) {
case PostLoadOperationType.MESSAGE:
dispatch(setPostLoadMessage(operation.data));
break;
default:
return;
}
});
sessionStorage.removeItem("postLoadOperations");
}
}
};
};
The service itself is a function that accepts dispatch
as a parameter, and returns push
and run
function in an object.
It manages the persistence layer for the post-load operations, and provides interface to operate on it. Let's dissect and investigate the pieces.
const operations = JSON.parse(sessionStorage.getItem("postLoadOperations"));
First, when the service is invoked, it checks for a sessionStorage
entry that contains a key postLoadOperations
.
push(type, data) {
sessionStorage.setItem("postLoadOperations", JSON.stringify(operations ?
operations.concat({type, data}) : [{type, data}]));
},
The push
function, like the name suggests, pushes a post-load operation to the operation queue. It checks whether a queue exists or not. If a queue exists, it pushes the operation to the queue, or else, creates a queue and pushes the specified operation to the new queue instead.
run() {
if(operations) {
operations.forEach(operation => {
switch(operation.type) {
case PostLoadOperationType.MESSAGE:
dispatch(setPostLoadMessage(operation.data));
break;
default:
return;
}
});
sessionStorage.removeItem("postLoadOperations");
}
}
And, run
contains the instruction for processing the operations. It acts somewhat similar to the Redux reducer. When the function is invoked, each operation is checked for a matching processor. If an operation type matches a processor, then the operation is executed, or else, left alone. Since we're considering a React-Redux app., in most of the cases we'll dispatch a lot's of action in the processor (case
) blocks. Finally, when all the operations are processed, the queue is removed, since it has a single shot use case.
<div className="home">
<span className="input-label">Message: </span>
<input className="form-control primary-input"
value={message}
onChange={e => this.setState({message: e.target.value})} />
<Button className="submit-button" onClick={e => onSpaReload(message)}>
Click me to reload the SPA!
</Button>
<div className="post-load-message">
Post-load Message: {postLoadMessage}
</div>
</div>
Let's focus on our UI. The primary UI can be found in Home
component. It's an ordinary component with a controlled text input
, a Button
and a div
region to display some text.
When the Button
is clicked, a callback is fired, that is defined in Redux's mapDispatchToProps
.
const mapDispatchToProps = dispatch => ({
onSpaReload(message) {
PostLoadOperations().push(PostLoadOperationType.MESSAGE, message);
location.reload();
}
});
Here, we are going to set a post-load operation and reload the SPA. push
, takes operation type as the first argument and operation data (data, that must be provided for a specific operation processor to work) as the second.
And, probably you have already figured out that, the postLoadMessage
prop is coming from home.js
reducer, and located in state.home.postLoadMessage
of the Redux state.
So far, we covered the operation queuing part. And, we know that, if some text is inserted in the input
and the Button
is pressed, a post-load operation will queue up and a SPA reload will trigger. But, how are we gonna process the operation then?
Here comes App.js
, the primary component of our app., that must be loaded in order for any child component in our app. to load (e.g. Home.js
). We latch a hook to our PostLoadOperations
service's run
method with the App
component's componentDidMount
lifecycle method.
componentDidMount() {
this.props.performPostLoadOperations();
}
const mapDispatchToProps = dispatch => ({
performPostLoadOperations() {
PostLoadOperations(dispatch).run();
}
});
So, basically what's happening here is, when our target SPA gets loaded / reloaded, it looks for the existence of any post-load operations. If any exists, they get executed. In case of our demo. when a SPA reload is triggered, the inputted text is dispatched along with the action setPostLoadMessage
, which in turn, gets displayed in the div
block of our UI.
dispatch(setPostLoadMessage(operation.data));
That's all there is to it. PostLoadOperations
service usage put negligible impact to the SPA performance, cause it starts batch processing once the App
component is mounted, not before that. Although, if other subroutines are expected in the componentDidMount
method of the App
component, then it would be a good idea to stack them up before the PostLoadOperations
to avoid any potentially significant execution delay caused by a huge operations queue .
All rights reserved