React has really great concepts. But when it comes to data management, everyone keeps coming up with more ridiculous methodologies and frameworks with attempts to create syntactic artwork.
I'll say it right now.
It's unreadable and overly complicated, more than it needs to be.
Oh, you think differently?
Let's start with the popular Redux for React, with the most basic example.
export const setVisibilityFilter = filter => ({
type: 'SET_VISIBILITY_FILTER',
filter
})
//...
const visibilityFilter = (state = VisibilityFilters.SHOW_ALL, action) => {
switch (action.type) {
case 'SET_VISIBILITY_FILTER':
return action.filter
default:
return state
}
}
2 files, 12 lines of code, one purpose, set the visibility filter value. And it is still incomplete! We have to add the reducer to the store, import the actions wherever we want to use them, all the while VSCode is just asking, huh? what?
But you might say, it's about having a predictable state container. Well, once you add thunks and start mixing state values, predictability flies out the window.
In addition, these reducers are simple, but in real-world applications, they are never as simple. They grow large, so you start breaking them up into functions, which don't fit nicely in the same file, so you create more files. Now you are bouncing around all these files just to manage one state of data.
Let's jump into Reacts version of redux, oh boy, settle in.
const initialState = {count: 0};
function reducer(state, action) {
switch (action.type) {
case 'increment':
return {count: state.count + 1};
case 'decrement':
return {count: state.count - 1};
default:
throw new Error();
}
}
function Counter() {
const [state, dispatch] = useReducer(reducer, initialState);
return (
<>
Count: {state.count}
<button onClick={() => dispatch({type: 'decrement'})}>-</button>
<button onClick={() => dispatch({type: 'increment'})}>+</button>
</>
);
}
Alright, a full example just for you. How many seconds did it take you to follow the code and all its purpose? You rockstars would probably say about 3-5 seconds. Well duh, you bathe in this all day.
Take a look at useReducer
. This provides all the technology for mutating the state of your component. What would happen to the code if we need to use, say, 2 or 3 different states. Now you've introduced some serious ugliness...
const [state1, dispatch1] = useReducer(reducer1, initialState1);
const [state2, dispatch2] = useReducer(reducer2, initialState2);
const [state3, dispatch3] = useReducer(reducer3, initialState3);
You better not use that naming.
Does anyone even useReducer? This becomes a formatting nightmare to manage all the reducers, just with this example using 12 different named variables. The amount of naming you have to do will just grow larger the more code integration you attempt to perform.
The next ridiculous is with React's Context...
const ThemeContext = React.createContext('light');
class App extends React.Component {
render() {
return (
<ThemeContext.Provider value="dark">
<Toolbar />
</ThemeContext.Provider>
);
}
}
It's more readable. But, we are forcing data to have a relationship with a specific component in a parent/child fashion. This is not ideal in real-world, where business requirements change frequently, and you end up having to heavily refactor to fit in some weird edge case.
class ThemedButton extends React.Component {
static contextType = ThemeContext;
render() {
return <Button theme={this.context} />;
}
}
Why would you do this to yourself. You basically created a global variable that has to be individually referenced for each type of context! What if you need 10 different context categories. Let me play my violin for you while you figure out how to best format it, for the next few days.
Let's move on to MobX...
class ObservableTodoStore {
@observable todos = [];
@observable pendingRequests = 0;
constructor() {
mobx.autorun(() => console.log(this.report));
}
@computed get completedTodosCount() {
return this.todos.filter(
todo => todo.completed === true
).length;
}
}
const observableTodoStore = new ObservableTodoStore();
Annotations, annotations, annotations. These are eyeball magnets in any language, but some people love them, so they get a pass for now. At least we are starting to get back on track with the time tested Services-oriented programming.
@observer
class TodoList extends React.Component {
render() {
const store = this.props.store;
return (
<div>
{ store.report }
<ul>
{ store.todos.map(
(todo, idx) => <TodoView todo={ todo } key={ idx } />
) }
</ul>
{ store.pendingRequests > 0 ? <marquee>Loading...</marquee> : null }
<button onClick={ this.onNewTodo }>New Todo</button>
</div>
);
}
onNewTodo = () => {
this.props.store.addTodo(prompt('Enter a new todo:','coffee plz'));
}
}
ReactDOM.render(
<TodoList store={ observableTodoStore } />,
document.getElementById('reactjs-app')
);
This seems a bit cleaner right. Except, now you have to manage passing your store and its data down the hierarchy again like the Context example above. This went backwards pretty fast. This is the reason Redux came out, to avoid having to trickle down your data manually.
That being said, I do enjoy the Services nature to having direct access to the methods and data with no exotic formating.
Can all of this be done better? Maybe... I wasted my weekend prototyping my ideal setup, but this is not a problem that can be easily solved by a single person.
Here is an example of what I mashed together...
//Run a query against DuckDuckGo API
export async function SearchDuckDuckGo(query) {
let url = 'https://api.duckduckgo.com/?t=flatstoreExample&format=json&q=' + query;
try {
let response = await axios.get(url);
let results = ReduceResults(response); //grabs only the results
flatstore.set("ddg", response.data);
flatstore.set("ddgQuery", query);
flatstore.set("ddgResults", results);
flatstore.set("ddgResultCount", results.length);
flatstore.set("ddgError", false);
}
catch (error) {
console.log(error);
flatstore.set("ddgError", error);
}
}
Focus is on readability and usability. A simple action to Search DuckDuckGo. It does its work, then saves the data in key/value format.
Ok, great, you the man, now what about showing it? Well, I played my violin over the weekend thinking about it, and came up with something like this...
class SearchStatus extends React.Component {
render() {
if (this.props.ddgError)
return (
<div style={{ color: '#f00' }}>
{this.props.ddgError.message}
</div>
);
return (
<div>
<i>
Searched {this.props.ddgQuery}
with {this.props.ddgResultCount || 0} results.
</i>
</div>
);
}
}
export default flatstore.connect(['ddgQuery', 'ddgResultCount', 'ddgError'])(SearchStatus);
Redux was brilliant in using a higher-order component. This allows you to remove all the framework craziness away from a component, and let the magic be done in the background.
In that respect, I stole it. But, we just want specific data points, so why not allow the user to directly specify what keys we need without circlejerking yourself.
I couldn't help myself, I had to go further. Real-world applications get complicated fast with all the business requirements coming from three or four levels above you. We need dynamic control, so we are back again to getting inspiration from redux's connect prop mapping.
class TodoResult extends React.Component {
render() {
return (
<div className={this.props.completed ? "completed" : ""}
onClick={() => { todoToggleComplete(this.props.id) }}>
<span className="result-title">{this.props.desc}</span> -
<span className="result-date">{this.props.dateCreated}</span>
</div >
);
}
}
let onCustomWatched = (ownProps) => {
return ['todos-' + ownProps.id];
}
let onCustomProps = (key, value, store, ownProps) => {
return {
...value
}
}
export default flatstore.connect([], onCustomWatched, onCustomProps)(TodoResult);
Except, this time we are limiting the onCustomProps to only those keys we specifically are watching. I even added object drill down, so I can watch a sub-item of the main "todos" object. React is about reacting only when needed, so I tried to only react when the components relevant data changes, with minimal coding effort for the developer.
I spend a lot of time teaching React, so most of this rant comes from what I see is confusing the new developers. There are many misunderstandings with coding in React, due to the complexity of modern JavaScript syntax used by the latest frameworks. It achieves very little, with so much code and files.
I was happy with the result of my prototype called flatstore, but its no where near usable in the real-world, so it'll be another one of my new projects that gets to ferment on GitHub.
In the meantime, I'll be wishing for one of you geniuses to bring back simplicity to programming.
Top comments (21)
In the first few lines of this article you quickly equate how complicated Redux can be with how complicated react is. React in itself can be rather simple and elegant. Adding a whole separate Library called Redux doesn't make react itself more complicated. It's the library that you're adding.
React's own State Management, context api, could be a bit intimidating at first too, but less than redux. Which is why you shouldn't use any state management at all when starting to learn react. You should probably just start with simple components, passing the state through props, and slowly add more complicated things.
Also, with a modest amount of patient reading all of the code samples you showed can be understood. All it takes is a little bit of JavaScript literacy and some time.
It didn't become as popular as it is because of unreadability and needless complication. Like any other programming concept all it takes is time, patience, and capacity to learn.
You make great points. I should have started with my React examples.
But the examples I have shown are not ideal for actual applications. There is a constant need to cross reference data, and it should be ready to happen at any time.
I feel like many frameworks try to have you hardcode data relationships against each component.
If we lock ourselves in, we just end up having to refactor several parts to add the new feature. For instance, NoSQL is popular for modern applications, but then we turn 180 degrees and lock down data in our applications.
If we can freely access any data at any time, it might increase chaos, but coding time for wild requirements can be handled quickly.
It's just that, React is a simple UI library. How you get and manage data is really up to you. It can be whatever you want it to be.
I think react became very popular because of 2 things: the concepts it introduced, and there were no better alternatives at the time (before vue and angular 2+). I write react, and it is needlessly complicated. The pro is that you'll become a better js developer, the con is weeping and gnashing of teeth.. Wrote a small app using svelte and what a breath of fresh air! I think the svelte way of doing things is the future
Good post! I agree that a react state management can be complicated. To make it easier, you can use an approach the smart/dumb component, where about state know only smart components.
For myself i find that the less state in the app, the more comprehensible the code will be. Is it cool when you state in one place and not spread at all app, for this case i prefer Akita. It is more convenient, OOP-like state management, you can read about this here.
That's a nice post despite being a noobie. I've searched this article just to rant on React, learning it just to learn something new but now I actually feel a bit more motivated because it seems like I'm not alone in this frustration! :D
Also, Joel, do you still stand for this, or do you feel like React developers created solutions for these issues in the last months?
What if you push (almost) all the responsability to the actions?
You could tweak redux a little bit by writing a middleware.
Then create a very generic reducer.
And now you can have actions like this:
This has lots of potential! Thank you for sharing.
With a bit more tweaks this could be very clean for developers to use without much thought. I will play with this trick thanks!
You're so right!
I love Python, Django and Django REST Framework because I can easily setup a basic API backend in under an hour, with multiple models, views, serializers, permissions and authentication. I can do a lot very quickly.
However, React seems like it requires so much more code just to do something that should be incredibly simple and basic. If you want to manage state in any meaningful way, you need Redux, which adds several extra files to essentially create a global dictionary and some functions to interact with it.
Why must it be so complicated? It really needn't be.
I don't think it helps that Javascript is an absolutely miserable language to use. The fact that there's nothing to replace it means that we've got to rely on a truly frustrating tool.
I feel similar... though I was able to understand ES6 and React easily, but it is very difficult for me to retain that information in my mind.
But, Python and Django have such readable code and easy to work on, and the information is very easy to retain.
Currently, I am trying to learn how to setup api with django rest framework and use that with React.
Hi @joetex ! Thank you for your article. When first encountering React years ago, I felt it was way too complex for what it offered - years later, I still think the same, although I've come to appreciate its merits more, particularly compared to previous frameworks (notably, Angular).
I lay absolutely no claim to being a genius (I'm honestly closer to being the opposite of one), but I have spent a few years developing a framework focused on simplicity and understanding. There might be a small probability you'd be interested in it. I've finally released a version that I feel it's mature enough (at both a conceptual and implementation level) to bother other humans to perhaps take a look: github.com/fpereiro/gotob
The framework uses a single global object as its sole repository of info, exactly as Flatiron/Flatstore.
Thank you again for your article!
... You are welcome
This just in, a new business requirement! Add users name entered from another component. And based on their role, they increment by 2.
Your setup is cleaner though!
Add the user state to a common ancestor. Pass the user object down as props.
State management is by far the hardest part of frontend development. But react offers some if the best tools out there.
And there are still devs out there that claim, React is as simple as just writing some HTML with a few JS code in between.
Might be true if all you do is a todoList. But want to start React in a more complex manner? You get stuck in that state thing.
I had the "luck" to get pushed into React (which is completely new to me) with writing an app that let's the user construct designs. It uses external libs, http-calls, backend connection, single sign-on requirements ect. And all that needs to be passed around into different children and then you want to write it in a way, that you know in 3 years wtf you did back then. I still want to see someone who does that with "just write some HTML"...
Please take a look at htmx. Brings me tears of joy for its simplicity. And for ux interactions, Alpine and hyperscript are simply awesome. No cumbersome webpack configurations, no packaging, no nonsense. Just plain and simple instructions of code. So you can spend more time improving your project and less time fiddling with non essential tasks.