When building a single-page application, managing state is important, but doing so in an efficient and DRY way can be difficult. My company's app, Graphite, is built in React, so early on, my first inclination was to use Redux. Redux is not specific to React, but it is heavily used in React application. In fact, it is generally considered the de-facto global state management tool. But when I first started building Graphite, I had no idea what I was doing. So, Redux was a complete mystery. It was a nightmare to understand, and it led to bugs I couldn't diagnose.
Fortunately, I got better at programming. Yet, even with knowledge and experience in tow, I still chose not to implement Redux when deciding on a more elegant solution than what I had (passing props through Higher-Order Components). First, let me tell you about ReactN, the solution I ultimately went with, and then I'll walk you through why I think it is better than Redux for many applications.
ReactN, simply, is React as if React handled global state natively. Of course, with Hooks, React sort of does handle state natively now, but ReactN even supports Hooks and can extend them. Rather than complex reducers and action creators that ultimately lead toward updating state in a store, ReactN lets you, the developer, decide when and how you update your application's global state. Here's a simple example before I dive into the more complex comparison of ReactN and Redux. From within your index.js
file in your react app, you would simply initialize your state like this:
import React, { setGlobal } from 'reactn';
setGlobal({
value: "Hi"
});
ReactDOM.render(<App />, document.getElementById('root'));
Then, from literally any component or helper file, you can update state. Here's what it would look like from a helper file (as opposed to a React Component):
import { getGlobal, setGlobal } = 'reactn';
export function sayBye() {
const currentVal = getGlobal().value;
if(currentVal === "Hi") {
setGlobal({ value: "Bye" });
}
}
Note that when fetching the current state outside of a Component, you'll use getGlobal(). When updating state withing a Class Component, you will already have access to the current value and can update it like this:
import React, { setGlobal } = 'reactn';
export default class Hello extends React.Component {
const { value } = this.global;
render() {
return (
<div>
<div>
<h1>{value}</h1>
</div>
{
value === "Hi" ?
<button onClick={() => setGlobal({ value: "Bye" })}>
Say Bye
</button> :
<button onClick={() => setGlobal({ value: "Hi" })}>
Say Hi
</button>
}
</div>
);
}
}
You'll see an example late of how to access state in a Function Component.
See how simple that is? It feels just like updating state from within a Component, but it's accessible anywhere in your app. You can access it independently in helper function. You can access your state in other Components. It works the way global state management should work.
Of course, you Redux buffs out there are probably looking at this saying "Redux does all that and more." And you'd be right. Redux absolutely does this. It does a whole lot more, too. And for most application that more is completely unnecessary. In fact, it likely leads toward harder to manage and harder to debug code.
The above examples are really simple, but you can see ReactN in action by crawling through the Graphite repository here. Or, of course, you could read the docs.
But I don't think I've convinced you yet with my simple examples. So, now, we'll build Redux's todo app tutorial in React and build the same todo app using ReactN. For simplicity, I won't go through every file, but I will link to the full repositories for both apps, and I will embed both apps in this blog post so that you can compare.
Let's start with the Redux version's file structure, taken directly from Redux:
Just looking at that src folder, I can already see two folders that will not show up in the ReactN version of this app: actions
and reducers
.
To keep things fair, I will build the ReactN todo app with the same component/container folder structure used by the Redux folks.
Let's start by comparing the index.js
file for each version of the app. This file is housed in the src
folder. First, here's the ReactN app:
import React, { setGlobal } from 'reactn';
import ReactDOM from 'react-dom';
import App from './components/App';
setGlobal({
todos: [],
filteredTodos: [],
filterBy: "all"
});
ReactDOM.render(<App />, document.getElementById('root'));
As you saw in my earlier example, initial state is instantiated in the index.js file and flows through every component. Now, here's the Redux version of the index.js
file:
import React from 'react'
import { render } from 'react-dom'
import { createStore } from 'redux'
import { Provider } from 'react-redux'
import App from './components/App'
import rootReducer from './reducers'
const store = createStore(rootReducer)
render(
<Provider store={store}>
<App />
</Provider>,
document.getElementById('root')
)
We can already see some confusing differences and we haven't even dove into the actual app yet. The Redux version has the App.js
Component wrapped in <Provider>
tags. There's also a reference to a store, and if you look at the import statements, there's a rootReducer
file being imported and passed to the store variable. Uh, what?
Sure, this all makes sense once you know Redux, but just ask yourself: Would you rather start with the ReactN index.js
file or the Redux one if you're basing your decision on complexity alone?
Since I'm not going to go through each file and compare, let's just look at two more files. The AddTodo.js
Component and the file(s) that actually manages todo actions. First, here's how we add Todos in the Redux app:
import React from 'react'
import { connect } from 'react-redux'
import { addTodo } from '../actions'
const AddTodo = ({ dispatch }) => {
let input
return (
<div>
<form onSubmit={e => {
e.preventDefault()
if (!input.value.trim()) {
return
}
dispatch(addTodo(input.value))
input.value = ''
}}>
<input ref={node => input = node} />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
export default connect()(AddTodo)
Some of this is pretty straightforward. We have a form. We are preventing the default submit action on the form. But then...what? We have a dispatch function that is calling another function? We're also having to add a ref element to our input field?
Ok, now, here's how it looks in the ReactN app:
import React from 'reactn'
import { addTodo } from '../helpers/todos';
const AddTodo = () => {
return (
<div>
<form onSubmit={(e) => addTodo(e)}>
<input id='todo-input' />
<button type="submit">
Add Todo
</button>
</form>
</div>
)
}
export default AddTodo;
How much simpler is that? We have a form that on submit calls the addTodo function. So simple.
Ok now what's actually happening when you add a todo, toggle todo completeness, and filter todos? Well, it depends on whether you're using Redux or ReactN. In Redux, those actions happen across four files for a total of 65 lines of code. With the ReactN app, all of those actions happen in a single file for a total of 45 lines of code. Sure 65 lines versus 45 lines isn't a huge difference, but this is a tiny app. The disparity grows as your app grows more complex.
Let's take a quick look at the two main files in the Redux app that handle adding a todo and filtering. First, here is the actions/index.js
file:
let nextTodoId = 0
export const addTodo = text => ({
type: 'ADD_TODO',
id: nextTodoId++,
text
})
export const setVisibilityFilter = filter => ({
type: 'SET_VISIBILITY_FILTER',
filter
})
export const toggleTodo = id => ({
type: 'TOGGLE_TODO',
id
})
export const VisibilityFilters = {
SHOW_ALL: 'SHOW_ALL',
SHOW_COMPLETED: 'SHOW_COMPLETED',
SHOW_ACTIVE: 'SHOW_ACTIVE'
}
We're doing a lot in this file. But then we're sending all that work to another file for processing (the reducers folder handles this). Here's the reducers/todos.js
file:
const todos = (state = [], action) => {
switch (action.type) {
case 'ADD_TODO':
return [
...state,
{
id: action.id,
text: action.text,
completed: false
}
]
case 'TOGGLE_TODO':
return state.map(todo =>
(todo.id === action.id)
? {...todo, completed: !todo.completed}
: todo
)
default:
return state
}
}
export default todos
And the reducers/visibilityFilters.js
file:
import { VisibilityFilters } from '../actions'
const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
export default visibilityFilter
This is where all of Redux's complexity comes in. There is so much going on here, you are surely going to need to reach for Redux's documentation unless you have A LOT of experience using Redux. That complexity is useful in some case, but not for most apps. Here's the file in the ReactN app that handles all of the todo actions, filtering, and state updates:
import { setGlobal, getGlobal } from 'reactn';
//Create new todos
export class Todo {
constructor(id, todo, status) {
this.id = id;
this.todo = todo;
this.status = status;
}
}
export function todoFilter(filterBy) {
setGlobal({ filterBy });
let todos = getGlobal().todos;
if(filterBy === "all") {
setGlobal({ filteredTodos: todos})
} else if(filterBy === 'active') {
const activeTodos = todos.filter(todo => todo.status === 'active');
setGlobal({ filteredTodos: activeTodos });
} else if(filterBy === 'complete') {
const completedTodos = todos.filter(todo => todo.status === 'complete');
setGlobal({ filteredTodos: completedTodos });
}
}
export function addTodo(e) {
e.preventDefault();
let todos = getGlobal().todos;
const filterBy = getGlobal().filterBy;
let todoField = document.getElementById('todo-input');
let newTodo = new Todo(Date.now(), todoField.value, 'active');
todos.push(newTodo);
let filteredTodos = filterBy !== "all" ? todos.filter(todo => todo.status === filterBy) : todos;
document.getElementById('todo-input').value = "";
setGlobal({ todos, filteredTodos });
}
export function toggleCompleteness(id) {
let todos = getGlobal().todos;
let filterBy = getGlobal().filterBy;
let thisTodo = todos.filter(todo => todo.id === id)[0];
thisTodo.status === "active" ? thisTodo.status = 'complete' : thisTodo.status = 'active';
let filteredTodos = filterBy !== "all" ? todos.filter(todo => todo.status === filterBy) : todos;
setGlobal({ todos, filteredTodos });
}
It might just be me, but that file is immensely more readable than all of the Redux app files combined. We could even DRY that code up a tiny bit more and shave off some lines, but I didn't feel like it was necessary for this demonstration.
So, what does this all mean? Should we stop using Redux altogether? Definitely not. Redux has its place. The problem is many new and seasoned JavaScript developers immediately reach for Redux without considering alternatives. Let's look at the order of consideration I'd recommend for global state management:
- Is my app small enough that I can simply pass state through Higher-Order Components? (no dependencies here).
- Am I working with a team small enough to ensure that updates to code that touches state won't get convoluted? (use ReactN).
- Am I working on a large app on a large team where updates to code that touches state would otherwise be unmanageable? (Use Redux or MobX or some other large state management framework)
Stumbling across Charles Stover's ReactN package was one of the most freeing things I've experienced as a developer thus far. I desperately did not want to manage the complexities of Redux in my app, and frankly, I didn't need to. But my app had grown large enough to not be able to easily support passing state props through components. If you are in a similar situation, explore ReactN. It's simple and powerful. And it does the logical thing with global state management.
If you'd like to explore the source code for the ReactN todo app, you can do so here.
And here is the code in action:
Top comments (4)
I have postponed to learn Redux until I have to deal with global state. Now that I have just come to this situation, I don't turn into Redux, but ReactN, because I have read Charles Stover article a couple of months ago. I have just tried the package, and it worked as expected. Its simplicity makes me feel like coding in a desktop non-web app in which dealing with global states/variables is not a problem, like using Visual Basic 6/VB.Net. :):)
I handle my project by myself, so ReactN should be enough for me.
However, if I have time, I think I will probably learn Redux too.
I am using redux or you can say implementing it, but facing too much time wastage while implementing a simple functionality. All are setup codes. I have tried reactn and want this library grow more. I love the concept and want to share it more that everyone use it and also fork it for better future usage.
My only question is that what will be the problems if I use it in big dashboard type of react js apps?
I have tried a example with ReactN with React-router-dom simple and easy. youtube.com/watch?v=PS23B4N3ioc
What about sagas? Or how do side effects factor into these state updates?