/reactjs-hooks-rxjs

Simple example of RxJS usage with ReactJS Hooks

Primary LanguageJavaScript

ReactJS 16.7 Hooks + RxJS

The ReactJS hooks feature introduced by the first 16.7 alpha release is being considered the new "state-of-the-art" of the React. The goal is to provide a more simple way to manage component’s internal state and lifecycle as an alternative to class components.

Adventurers javascript developers may wish to use RXJS to go full reactive instead of redux. But who try to use pure RXJS and React faces a boring boilerplate: subscribing and unsubscribing to observables and setting changes to component internal state, for EVERY connected component.

import React, { Component } from 'react';
import fooStore from './fooStore';

class Foo extends Component {

  state = {
    name: ''
  };
  
  componentWillMount() {
    this.subscription = fooStore.name.subscribe(name => this.setState({name}));
  }
  
  componentWillUnmount() {
    this.subscription.unsubscribe();
  }
  
  [...]

}

Some developers appeal to decorators to avoid this, but this is one of the known causes of the “component tree wrapper hell” that mobilized the React Team to introduce the hooks feature.

They also appeal to other libraries like “Recycle JS” that abstracts away the observable subscription/unsubscription.

The magical hook

No more extra libraries or decorators. Just one hook and your components are ready to react to any RXJS Observable changes:

import {useState, useEffect} from 'react';

function useObservable(observable, initialValue) {
  const [value, setValue] = useState(initialValue);

  useEffect(() => {
    const subscription = observable.subscribe(newValue => {
      setValue(newValue);
    });
    return () => subscription.unsubscribe();
  }, []);

  return value;
}

function FooComponent() {
  
  const value = useObservable(fooObservable, 'Hello!');
  
  // render it :)
  
}

It abstracts away the observable subscription and unsubscription and subscribes to observable changes updating it to component internal state, pretty simple huh?

Task list example

To be more practical, let's implement a classic TODO List example using ReactJS and RXJS.

Task store

Here we control the task list state as an application state.

import { BehaviorSubject } from "rxjs/internal/BehaviorSubject";

const taskStore = {
  __sequence: 0,
  __tasks: [],

  tasks: new BehaviorSubject([]),

  add(task) {
    task.id = taskStore.__sequence++;
    taskStore.__tasks = [...taskStore.__tasks, task];
    taskStore.tasks.next(taskStore.__tasks);
  },

  switchDone(id) {
    taskStore.__tasks = taskStore.__tasks.map(task => {
      if (task.id === +id) {
        return {
          ...task,
          done: !task.done
        };
      }
      return task;
    });
    taskStore.tasks.next(taskStore.__tasks);
  },

  remove(id) {
    taskStore.__tasks = taskStore.__tasks.filter(task => task.id !== +id);
    taskStore.tasks.next(taskStore.__tasks);
  }
};

export default taskStore;

Task list component

Component to display the task list and trigger the "switch done/undone" and "remove" actions to parent component.

import React from "react";

function TaskList({ tasks = [], onSwitch, onRemove }) {

  function handleSwitchClick(e) {
    if (typeof onSwitch === "function") {
      const { name: taskId } = e.currentTarget;
      onSwitch(+taskId);
    }
  }
  
  function handleRemoveClick(e) {
    if (typeof onRemove === "function") {
      const { name: taskId } = e.currentTarget;
      onRemove(+taskId);
    }
  }

  return (
    <ul>
      {tasks.map(task => (
        <li
          key={task.id}
          style={{ textDecoration: task.done ? "line-through" : "unset" }}
        >
          <span>{task.description}</span>
          <button 
            name={task.id} 
            type="button" 
            onClick={handleSwitchClick}
          >
            {task.done ? "Undone" : "Done"}
          </button>
          <button 
            name={task.id} 
            type="button" 
            onClick={handleRemoveClick}
          >
            Remove
          </button>
        </li>
      ))}
    </ul>
  );
}

export default TaskList;

Task form component

Component to provide a "description" field and a "submit" event.

import React from "react";

function TaskForm({ onSubmit }) {
  const [description, setDescription] = useState("");

  function handleDescriptionChange(e) {
    setDescription(e.target.value);
  }

  function handleSubmit(e) {
    e.preventDefault();
    setDescription("");
    if (typeof onSubmit === "function") {
      onSubmit({ description, done: false });
    }
  }

  return (
    <form onSubmit={handleSubmit}>
      <input
        type="text"
        name="description"
        onChange={handleDescriptionChange}
        value={description}
      />
      <button type="submit">Add</button>
    </form>
  );
}

export default TaskForm;

App component

Finally, the App component, where everything are controlled.

import React from "react";
import TaskForm from './task/TaskForm';
import TaskList from './task/TaskList';
import taskStore from './task/taskStore';

function App() {
  const tasks = useObservable(taskStore.tasks, []);

  return (
    <div>
      <TaskForm onSubmit={taskStore.add} />
      <TaskList 
        tasks={tasks} 
        onSwitch={taskStore.switchDone} 
        onRemove={taskStore.remove}
      />
    </div>
  );
}

export default App;