In building out web applications using React.js, I've had to manage both component and application state. While the component state is managed with built-in methods, the application state is managed using tools like Redux.
How does Redux work? The documentation talks about actions, constants, and reducers. Which I and much everyone else uses. However, I'd struggled to internalize this concept and how it's all put together.
I recently asked Meabed to explain to me in his terms, how state management works and he did just that. I'm writing to you to explain using an HTML file and the browser window
object, how state management tools possibly like Redux work, with stores, constants, actions, subscriptions & updates, and reducers.
All these will be done on Codesandbox and you can find the final sandbox here.
Create the HTML file
I created a simple index.html file and opened it in the browser (no bundler required). The file contains the following:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<meta http-equiv="X-UA-Compatible" content="ie=edge">
<title>Static Template</title>
</head>
<body>
<h1>Building out state from Scratch using a counter and input</h1>
</body>
</html>
Create static HTML elements
We require 2 script tags, one before the body
element to load Javascript before the document loads, and another after the document is loaded. The first will manage the state logic and the second will update the page. Also, we will demonstrate the state update using a counter with 2 buttons and an input field. With these we have:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<meta http-equiv="X-UA-Compatible" content="ie=edge" />
<title>Static Template</title>
</head>
<body>
<script>
// Handle State management
</script>
<h1>Building out state from Scratch using a counter and input</h1>
<button id="increment">Increment</button>
<hr />
<p id="counter_data">{counter} - {name}</p>
<hr />
<button id="decrement">decrement</button>
<hr />
<input type="text" placeholder="Enter your email" />
<script>
// Update DOM
</script>
</body>
</html>
We created a simple HTML document with 2 buttons, a counter - name display, and an input element. The goal is to increment and decrement a counter value (which we will assign shortly), and update the {name}
value with whatever is entered in the input.
You may wonder why we have to go through this long process to handle increments and decrements. You are right. For small applications like counters, handling an application state is trivial, as a single JS file is sufficient. However, in larger projects, there is a need to organize and manage the flow of data throughout the components.
How state management works (theoretically)
In clear steps, we will handle the state in this app with:
- Creating a data store in the window object that is accessible everywhere in the browser
- Create a function to update the DOM (the fancy term is 'render engine')
- Create functions to update the store data (these are actions)
- Define a new store data in the function to update the store (this is a reducer)
- Create a global function that receives function calls to update the store along with any provided data. It updates the store and re-renders the webpage.
Technologies like React and Redux work to optimize this process and enhance the development experience.
Creating a data store
In the opening script element, we will create an object as a data store in the window
object.
[...]
<body>
<script>
// Handle State management
window.store = {counter: 0, name: "William"}
</script>
<h1>Building out state from Scratch using a counter and input</h1>
<button id="increment">Increment</button>
<hr />
<p id="counter_data">{counter} - {name}</p>
<hr />
<button id="decrement">decrement</button>
<hr />
<input type="text" placeholder="Enter your email" />
<script>
// Update DOM
</script>
</body>
[...]
Create a render function for the DOM
A quick render function will replace specific portions of an identified DOM node value with variables from the store. In the second script tag before the closing body tag, we have:
<body>
<script>
// Handle State management
window.store = { counter: 0, name: "William" };
</script>
<h1>Building out state from Scratch using a counter and input</h1>
<button id="increment">Increment</button>
<hr />
<p id="counter_data">{counter} - {name}</p>
<hr />
<button id="decrement">decrement</button>
<hr />
<input type="text" placeholder="Enter your email" />
<script>
// Update DOM
window.originalData = window.originalData || document.getElementById("counter_data").innerHTML; // Store original state before state changes, required for rerender
// Render function
function renderData() {
document.getElementById(
"counter_data"
).innerHTML = window.originalData
.replace("{counter}", window.store.counter)
.replace("{name}", window.store.name);
}
renderData();
</script>
</body>
We created a render function with a basic template engine (hell yeah!) which replaces {counter}
and {name}
with data from the global store. With the data from the store, the page looks like:
Create functions(actions) and reducers to modify data
To increment, decrement, and update the page, we will create functions that update the store data. In the first script element, we create 3 functions having:
<script>
// Handle State management
window.store = { counter: 0, name: "William" };
// Create functions
function increment() {
// reducer
window.store.counter += 1;
}
function decrement() {
window.store.counter -= 1;
}
function setName(newName) {
window.store.name = newName;
}
</script>
We have increment
, decrement
and setName
functions to increment, decrement, and update the name data respectively. Also, for now, the expression in the actions is just to update the store data.
Call actions on button click and input change
The next step is to call the actions on button click and input change. We update the buttons and input then rerender the element for each action completion. We now have:
<script>
// Handle State management
window.store = { counter: 0, name: "William" };
// Create functions
function increment() {
// reducer
window.store.counter += 1;
renderData();
}
function decrement() {
window.store.counter -= 1;
renderData();
}
function setName(newName) {
window.store.name = newName;
renderData();
}
</script>
<h1>Building out state from Scratch using a counter and input</h1>
<button id="increment" onclick="increment()">Increment</button>
<hr />
<p id="counter_data">{counter} - {name}</p>
<hr />
<button id="decrement" onclick="decrement()">decrement</button>
<hr />
<input type="text" placeholder="Enter your email" onchange="setName(this.value)"/>
At this time, the counter works as well as the input object.
Immutability is a core part of how tools like Redux and React work, with those, the state is not mutated as we do at the moment. Here, we re-render the elements for every action, this has a huge performance overhead when managing a large application. Also with state-controlled from multiple app points, there is multi-directional data flow which could lead to data inconsistencies in an app.
Following these, state data should not be mutated, however, a new version of the state is created. This way, efficient render engines like in React.js know from comparing the previous state object and the new state object, when to render, and what portion of the app to rerender. Subsequently, you can look up "Shallow compare" and "Deep equality" of objects in JavaScript.
Create a sample redux store
To achieve immutability, we will create a store which has a function that:
- Dispatches an action
- Takes a data returned in the action (reducer)
- Merges it with the store data (root reducer)
In the opening script element we add the
window.reduxStore
object with:
[...]
<script>
// Handle State management
window.store = { counter: 0, name: "William" };
// redux store with dispatch
window.reduxStore = {
dispatch(action, data) {
const newData = window[action](data);
window.store = { ...window.store, ...newData };
renderData();
}
};
[...]
</script>
[...]
In the dispatch method, we receive action
and data
as parameters. Each action function to be 'dispatched' has a unique name, and when used in the dispatch function, it is used to call the action and assign it to a new variable called newData
.
The data sent in the dispatch function is passed to the action which is in turn used in the reducer. The result is spread along with the existing store data into new value for the store, rather than mutating/modifying the store itself.
With the re-rendering out of the way, we can clean up the action functions to:
<script>
// Handle State management
window.store = { counter: 0, name: "William" };
window.reduxStore = {
dispatch(action, data) {
const newData = window[action](data);
window.store = { ...window.store, ...newData };
renderData();
}
};
// Create functions
function increment() {
// reducer
return { counter: (window.store.counter += 1) };
}
function decrement() {
return { counter: (window.store.counter -= 1) };
}
function setName(newName) {
return { name: newName };
}
</script>
Also, update the buttons and input to dispatch the actions while passing only the action name, which seems like a constant, sound familiar from react-redux? hehe.
<h1>Building out state from Scratch using a counter and input</h1>
<button id="increment" onclick="window.reduxStore.dispatch('increment')">
Increment
</button>
<hr />
<p id="counter_data">{counter} - {name}</p>
<hr />
<button id="decrement" onclick="window.reduxStore.dispatch('decrement')">
decrement
</button>
<hr />
<input
type="text"
placeholder="Enter your email"
onchange="window.reduxStore.dispatch('setName', this.value)"
/>
At this point, we have the data flowing from application state to components and state management completed using the window
object as a store. The buttons manipulate the resulting number on increment or decrement, whereas the input element updates the name field when you click out of the form input after a form entry.
Recap
We have:
- A button triggers a defined action function
- The action returns a reducer
- A new store is created with the new state data as the previous store data is immutable
- The DOM elements are re-rendered to reflect the updated state.
Tools like Redux and React-redux work to optimize every step of this process by having abstracted and clearly defined,
- Actions
- Constant
- Reducers
- Subscribers
- Rendering, as well as a host of optimization techniques.
You can find the complete code sandbox to this here
I hope this gives you a better understanding of how state management works. Also, this is just the base of the concept and you can read through multiple state management libraries for more insights.
Till next time.
William.
This article was originally published on Hackmamba.
Top comments (1)
Hy,
Great article
Thank you