How to manage state in event-driven JavaScript apps?

Content verified by Anycode AI
July 21, 2024
Effective state management in an event-driven application affects the continuity of experience for users. As your application grows, it becomes hectic to keep track of various state changes arising from interactions between a user, API responses, and other events. That could get you into conditions where state inconsistencies prevail, issues are hard to debug, and unmaintainable code with no structured approach. Specifically, this guide touches on managing state in JavaScript applications with React and Redux in quite granular steps. It explains how to set up a project, implement the management of local and global states, handle asynchronous events, improve performance, and guarantee maintainability. Based on this guide, large-scale applications developed will be reliable, scalable, and maintainable in an event-driven environment.

Handling state in event-driven JavaScript apps is crucial for building interactive and engaging web experiences. State is essentially the data that components or systems need to function properly. In our world of event-driven programming, state management means dealing with user actions, server responses, and other asynchronous events. Sounds fun, right?

Let's dive into some practical examples that use native JavaScript, and later, we’ll touch on how libraries like React and Redux help streamline this process. Don't worry, we’ll go step-by-step so everything makes sense.

Using Closures for State Management

Closures in JavaScript can help keep track of state efficiently.

function createStateManager() {
    let state = {};

    return {
        getState: function () {
            return state;
        },
        setState: function (newState) {
            state = {...state, ...newState};
            console.log("State has been updated:", state);
        }
    }
}

const stateManager = createStateManager();
stateManager.setState({name: 'John Doe'});
console.log(stateManager.getState()); // Output: { name: 'John Doe' }

stateManager.setState({age: 30});
console.log(stateManager.getState()); // Output: { name: 'John Doe', age: 30 }

Here, createStateManager hides the state and offers getState and setState methods for state interaction. Pretty neat, huh?

Using PubSub Pattern

The PubSub pattern is like having a bulletin board where different parts of your application can post updates and read notices. It's awesome for event-driven updates.

class PubSub {
    constructor() {
        this.events = {};
    }

    subscribe(event, callback) {
        if (!this.events[event]) {
            this.events[event] = [];
        }
        this.events[event].push(callback);
    }

    publish(event, data) {
        if (!this.events[event]) {
            return;
        }
        this.events[event].forEach(callback => callback(data));
    }
}

// Usage example
const pubsub = new PubSub();

pubsub.subscribe('stateChange', (newState) => {
    console.log('State has changed:', newState);
});

let state = {};

function setState(newState) {
    state = {...state, ...newState};
    pubsub.publish('stateChange', state);
}

setState({name: 'Jane Doe'});
// Console output: "State has changed: { name: 'Jane Doe' }"

setState({age: 28});
// Console output: "State has changed: { name: 'Jane Doe', age: 28 }"

The PubSub class makes it so different parts of your app can talk by publishing and subscribing to events. It’s like magic but with code.

Using React for State Management

React offers lovely ways to handle state with hooks like useState and useReducer.

Using useState

import React, { useState } from 'react';

function Counter() {
  const [count, setCount] = useState(0);

  return (
    <div>
      <p>You clicked {count} times</p>
      <button onClick={() => setCount(count + 1)}>
        Click me
      </button>
    </div>
  );
}

export default Counter;

Using useReducer

For more complex state logic, useReducer is like having a mini-Redux directly in your component.

import React, { useReducer } from 'react';

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 (
    <div>
      <p>Count: {state.count}</p>
      <button onClick={() => dispatch({type: 'increment'})}>+</button>
      <button onClick={() => dispatch({type: 'decrement'})}>-</button>
    </div>
  );
}

export default Counter;

It's like having a tiny state machine specifically tailored for your component’s needs.

Complex State Management with Redux

When your app’s state management feels like herding cats, Redux comes to the rescue with a centralized store.

// actions.js
export const increment = () => ({
  type: 'INCREMENT'
});

export const decrement = () => ({
  type: 'DECREMENT'
});
// reducers.js
const initialState = { count: 0 };

export function counterReducer(state = initialState, action) {
  switch (action.type) {
    case 'INCREMENT':
      return { count: state.count + 1 };
    case 'DECREMENT':
      return { count: state.count - 1 };
    default:
      return state;
  }
}
// store.js
import { createStore } from 'redux';
import { counterReducer } from './reducers';

const store = createStore(counterReducer);

export default store;
// CounterComponent.js
import React from 'react';
import { useSelector, useDispatch } from 'react-redux';
import { increment, decrement } from './actions';

function CounterComponent() {
  const count = useSelector(state => state.count);
  const dispatch = useDispatch();

  return (
    <div>
      <p>Count: {count}</p>
      <button onClick={() => dispatch(increment())}>+</button>
      <button onClick={() => dispatch(decrement())}>-</button>
    </div>
  );
}

export default CounterComponent;
// App.js
import React from 'react';
import { Provider } from 'react-redux';
import store from './store';
import CounterComponent from './CounterComponent';

function App() {
  return (
    <Provider store={store}>
      <CounterComponent />
    </Provider>
  );
}

export default App;

This shows how Redux can handle state across multiple components seamlessly. It uses Actions and Reducers to define how the state changes, and the Store to keep the state.

Each method has its perks and uses, depending on your app’s complexity. By mastering these techniques, you can fine-tune your state management to match your app's needs perfectly.

Have any questions?
Our CEO and CTO are happy to
answer them personally.
Get Beta Access
Anubis Watal
CTO at Anycode
Alex Hudym
CEO at Anycode