React... Why so complicated...?

joetex

Joel Ruiz

Posted on October 3, 2019

React... Why so complicated...?

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
  }
}
Enter fullscreen mode Exit fullscreen mode

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>
    </>
  );
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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>
    );
  }
}
Enter fullscreen mode Exit fullscreen mode

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} />;
  }
}
Enter fullscreen mode Exit fullscreen mode

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();
Enter fullscreen mode Exit fullscreen mode

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')
);
Enter fullscreen mode Exit fullscreen mode

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);
    }
}
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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);
Enter fullscreen mode Exit fullscreen mode

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.

💖 💪 🙅 🚩
joetex
Joel Ruiz

Posted on October 3, 2019

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related