This page looks best with JavaScript enabled

Simple technique for state management with React and RxJS

 ·  ☕ 7 min read  ·  ✍️ Iskander Samatov

React and RxJS

In this post, let’s go over a simple technique for communicating between your components with React and RxJS.

Redux is the go-to library for handling state management for a lot of people. And while it’s a powerful tool, Redux should not be used as a swiss knife that solves all state management issues.

Oftentimes a leaner approach is preferred. For example, you could share the state using a simple observer pattern with RxJS. Communicating between your components this way has several notable advantages over Redux and Context API. I will list all of them at the end of the post.

RxJS for state management

RxJS is a popular library for Reactive Programming . RxJS helps write declarative code for handling side effects and asynchronous actions. To do so, it uses the concept of continuous data streams and subscriptions.

RxJS is a powerful tool with a lot of functionality. However, in this post, we will limit our use of RxJS to data streams for sharing the state between components.

Here’s the idea in a nutshell: the client subscribes to a data stream that continuously pushes new data as it arrives. The data stream can have multiple subscribers that subscribe/unsubscribe to it as they please.

The main RxJS objects we will be using:

  1. Subject - An object with the data stream.
  2. Observer - Consumer of the values emitted by the subject. In our case, it will simply be a function that updates the state as the new data arrives.

Here’s the diagram to illustrate the relationship between the Subject and Observer:

Observer pattern

Using React and RxJS

What we’re building

Let’s build a simple note-taking app. We will have two sibling components:

  1. Header - will contain controls for clearing and adding new notes.
  2. Content - will be responsible for displaying the notes.
note-taking app

Shared state management

Let’s start with adding the logic for using the shared state. Here’s our notesState.js file that handles the shared state:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import { useState, useEffect } from "react";
import { BehaviorSubject } from "rxjs";

let subject = null;

export const getNotes = () => {
  if (!subject) {
    return undefined;
  }

  return subject.value;
};

export const useSharedNotes = () => {
  const [notes, setNotes] = useState([]);

  if (!subject) {
    subject = new BehaviorSubject([]);
  }

  useEffect(() => {
    const subscription = subject.subscribe((notes) => {
      setNotes(notes);
    });

    return () => {
      if (subscription) {
        subscription.unsubscribe();
      }
    };
  }, []);

  function addNote(newNote) {
    subject.next([...notes, newNote]);
  }

  function clear() {
    subject.next([]);
  }

  return { addNote, clear, notes };
};

One thing to note: getNotes - is a utility function for getting the current state of the notes. You can use it anywhere in the app, not just React components.

useSharedNotes - a custom hook that you can use to get and mutate the current state of the notes. useSharedNotes uses useEffect method for subscribing to the subject when the component is loaded.

To subscribe, we use the subject.subscribe method. Our subscriber function updates the state whenever it receives new notes data. subject.subscribe returns a subscription object that we use to unsubscribe whenever the component is unmounted.

useSharedNotes returns addNotes, clear functions and the notes array that contains our current notes state. That’s all we need to get our app to work.

Components

Now let’s start adding the Header and Content components.
First, let’s define Header. This component contains controls for adding new notes and clearing them:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
import { useState } from "react";
import { useSharedNotes } from "./notesState";

export default function Header() {
  const [value, setValue] = useState("");
  const { addNote, clear } = useSharedNotes();

  const handleChange = (e) => {
    setValue(e.target.value);
  };

  const handleSendNoteClick = () => {
    addNote(value);
    setValue("");
  };

  const handleClearnNotesClick = () => {
    clear();
  };

  return (
    <div className="header">
      <input value={value} onChange={handleChange} placeholder="your note..." />
      <div className="buttons">
        <button onClick={handleSendNoteClick}>Send Note</button>
        <button onClick={handleClearnNotesClick}>Clear Notes</button>
      </div>
    </div>
  );
}

Pretty straightforward. We have input and two buttons for adding and clearing notes. We’re using the addNote and clear methods to push changes to the shared state.

Now let’s define the Content component that’s responsible for displaying the notes:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
import { useSharedNotes } from "./notesState";
import "./styles.css";

export default function Content() {
  const { notes } = useSharedNotes();
  return (
    <div className="content">
      {notes.map((note, index) => (
        <p key={index}>{note}</p>
      ))}
    </div>
  );
}

We’re only using the notes field from the shared state. That’s because this component’s sole job is to display the notes.

And that’s all there’s to it! As I said, a much leaner approach compared to Redux.

Refactoring the shared state

Our shared state code is okay for a simple app like ours. However, in a big project with multiple shared states, writing separate hooks for each state can be cumbersome.

Let’s make our notesState more reusable:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
import { useState, useEffect } from "react";
import { BehaviorSubject } from "rxjs";

const subjects = {};

export const NOTES_STATE = "NOTES_STATE";

function getNewSubject(initValue) {
  return new BehaviorSubject(initValue);
}

export const getSharedStateValue = (subjectname) => {
  if (!subjects[subjectname]) {
    return undefined;
  }

  const subject = subjects[subjectname];

  return subject.value;
};

export const useSharedState = (subjectname, initval) => {
  const [value, setValue] = useState(initval);

  if (!subjects[subjectname]) {
    subjects[subjectname] = getNewSubject(initval);
  }

  const subject = subjects[subjectname];

  useEffect(() => {
    const subscription = subject.subscribe((message) => {
      setValue(message);
    });

    return () => {
      if (subscription) {
        subscription.unsubscribe();
      }
    };
  }, [subject]);

  function set(val) {
    subject.next(val);
  }

  return [value, set];
};

Our refactored code is similar to the previous one. But now we have subjects map that contains multiple different subjects. We access them using statically defined keys.

We pass those keys to the useSharedState hook as the first parameter to specify which subject we’re subscribing to. For our note-taking app, we would use the NOTES_STATE key to access and mutate the shared state. For each new state, we can keep adding new keys and use them as needed.

Benefits of our approach

Let’s summarize the benefits of using shared state with React and RxJS:

  1. Leaner code. This approach to sharing state generally involves writing less code.
  2. Access the state from anywhere. With Redux and Context API, you can only access the global state from components or redux actions. With our shared state approach, you can use the getSharedStateValue to read the state from anywhere, including static util methods.
  3. Explicit side effects. Using the shared state approach makes it easier to track the side effects. With Redux, you would first need to find the reducers affected by the actions, then all of the components that subscribe to those reducers. With our approach, you can search for a subscription key (like NOTES_STATE), and you will immediately see all of the components subscribed to it.
  4. Avoids re-rendering the whole component tree. One advantage of this approach over Context API is that this will only re-render the subscribed components. With Context API, when there’s any change to the state, the whole component tree is re-rendered.

And that’s it for this post! Hopefully, you found this pattern to be simple and useful enough to try in your projects. For big applications, I suggest combining it with Redux. Use Redux for the global state that doesn’t change very often, like user sessions and tokens. And use RxJS for things that can change often and affect a smaller number of components.

Here’s a link to the code sandbox for this tutorial.

If you’d like to get more web development, React and TypeScript tips consider following me on Twitter, where I share things as I learn them.
Happy coding!

Share on

Software Development Tutorials
WRITTEN BY
Iskander Samatov
The best up-to-date tutorials on React, JavaScript and web development.