0.0.7-beta-1 • Published 6 years ago

rex-state v0.0.7-beta-1

Weekly downloads
26
License
MIT
Repository
github
Last release
6 years ago

🦖 Rex State

React States on Steroids 💉💊

The simplest state management tool for React. Built completely with React Hooks!

Build Status Maintainability Test Coverage

Version Downloads Bundlephobia

Star on GitHub Watch on GitHub Twitter Follow


PRs Welcome 👍✨

Motivation

React is a simple and straightforward library for building UI however, the current solutions require you to learn additional concepts and add more dependencies to your project.

Rex State aims to leverage the simplicity of React Hooks, letting you manage state without having to use any new concepts or huge dependencies and it also encourages good programming principles!

Requirements

Rex State is built purely on React Hooks hence it requires React > 16.8 to work.

Installation

yarn add rex-state

# or

npm i rex-state

Usage

import useRex, { createRexStore } from "rex-state";

API ﹣

Rex State is inspired by React's simplicity in building UI. Hence it borrows one of the most common React-ish style for creating & updating states.

useRex Hook

useRex is very similar to the React's own useState hook. However, it is built to work with objects just like state & setState from traditional react class components.

You can initialize useRex hook with an object such as ﹣

const [state, setState] = useRex({
  name: "",
  email: "",
  phone: ""
});

Now if you want to update only the name property, you can do ﹣

setState({ name: "John Doe" });

This works similar to how this.setState works in class components and updates the name property of your state. However, unlike the class components this operation is synchronous.

This is a classic React functional component with useState hook

import React, { useState } from "react";

const InputField = () => {
  const [value, updateValue] = useState("");

  return (
    <input
      type="text"
      value={value}
      placeholder="Add Text here..."
      onChange={event => updateValue(event.target.value)}
    />
  );
};

The above component will render a simple input field and will take care of updating the input state when a new value is entered in the input field. However, the state & UI are tightly coupled together and it is impossible to reuse the same state logic to a different component.

This is the same component using Rex State

import React from "react";
import useRex from "rex-state";

const useInput = () => {
  const [state, setState] = useRex({ value: "" });

  return {
    get value() {
      return state.value;
    },
    updateValue(value) {
      setState({ value });
    }
  };
};

const InputField = () => {
  const { value, updateValue } = useInput();

  return (
    <input
      type="text"
      value={value}
      placeholder="Add Text here..."
      onChange={event => updateValue(event.target.value)}
    />
  );
};

export default InputField;

The functionality of the component remains unchanged, however we now have two entities.

  • useInput is a hook which is used to define your application state.
  • InputField is a functional React Component that uses useInput hook to render it's UI.

This decouples the UI from the State and also provides a nice & familiar way to write an API to define & manage your states.

Try this example directly in CodeSandbox

Building a re-usable hook

The pattern that was used in the example code above lets you build re-usable hooks that can be easily shared across multiple components. Consider the following example where we will build a hook for a counter ﹣

const useCounter = () => {
  const [state, setState] = useRex({ count: 0 });

  return {
    get count() {
      return state.count;
    },
    increment() {
      setState({ count: state.count + 1 });
    },
    decrement() {
      setState({ count: state.count - 1 });
    }
  };
};

The useCounter hook contains the state for tracking your counter. It returns an object with three properties (including a getter for accessing the count)

This hook can now be easily added into any functional react component as follows ﹣

const Counter = () => {
  const { count, increment, decrement } = useCounter();

  return (
    <div>
      <button onClick={increment}>up</button>
      <span>{count}</span>
      <button onClick={decrement}>down</button>
    </div>
  );
};

Try this example directly in CodeSandbox

Since useRex hook is built only to simplify managing large state objects. You can follow this pattern without rex-state too!

createRexStore

createRexStore accepts your hook as the argument and returns an object with three properties ﹣

  • RexProvider which is a "Provider" component that will let you pass your hook down the React component tree to all the components by storing it in React context.
  • injectStore is an Higher Order Component (HOC) which will pass the required props from the hook in the React Context to your component.
  • useStore hook will fetch your hook from the React context into your current component. This is built on top of the react "useContext" hook and comes with it's own performance concerns. Read the performance section for more info.

Tutorial

Centralized state management with Rex State

Rex State simplifies building re-usable hooks. However, it also provides an easy to use API to build a centralized state that can be easily shared across your entire application.

tl;dr ﹣ The code in the below example available for you to try in CodeSandbox

Follow the below example to create a centralized state with Rex State ﹣

Building a ToDo app with Rex State

Let's start with building a simple todo app which contains a list of tasks and their completion status in the following format ﹣

[
  {
    task: "Learning React",
    isComplete: false
  },
  {
    task: "Learning Rex State",
    isComplete: false
  }
];

We'll start with creating a re-usable hook for our tasks ﹣

import useRex from "rex-state";

const useToDoList = () => {
  const [state, setState] = useRex({
    title: "Learning Frontend Development",
    tasks: [
      {
        task: "Learning React",
        isComplete: false
      },
      {
        task: "Learning Rex State",
        isComplete: false
      }
    ]
  });

  return {
    get title() {
      return state.title;
    },
    get tasks() {
      return state.tasks;
    },
    get tasksCount() {
      return state.tasks.length;
    },
    get completedTasksCount() {
      return state.tasks.filter(task => task.isComplete).length;
    },
    addTask(newTask) {
      const newTaskList = [
        ...state.tasks,
        {
          task: newTask,
          isComplete: false
        }
      ];
      setState({ tasks: newTaskList });
    },
    toggleTask(taskIndex) {
      const updatedTaskList = state.tasks.map((item, index) => {
        if (taskIndex === index) {
          return {
            task: item.task,
            isComplete: !item.isComplete
          };
        } else {
          return item;
        }
      });
      setState({ tasks: updatedTaskList });
    }
  };
};

Read about useRex in detail

Next step is to make this hook available to all components. Rex State comes with createRexStore module for this purpose which will create a store with your hook.

const { RexProvider, injectStore } = createRexStore(useToDoList);

Read about createRexStore in detail

React part of the App

We will be building 4 components in the ToDo List App.

  • <Title/> - Title of our list
  • <InputField /> - For adding new tasks
  • <TasksList /> - Listing the tasks with a toggle option
  • <TasksStats /> - For displaying the completed tasks count

Since all these components are dependent on the useToDoList hook we created earlier, we have to wrap all these components inside the RexProvider component

export default function App() {
  return (
    <RexProvider>
      <Title />
      <InputField />
      <TasksList />
      <TasksStats />
    </RexProvider>
  );
}

Title

We need the task title inside our title components. A regular title component will look something like this ﹣

const Title = ({ title }) => {
  return <h1>{title}</h1>;
};

Now the title prop of our component is basically the getter property title returned from the useToDoList hook. Here we can use the injectStore HOC to inject the title prop directly to the HOC as follows ﹣

const Title = injectStore("title")(({ title }) => {
  return <h1>{title}</h1>;
});

InputField

In this component we need to use the addTask method returned by our hook to add a new task.

const InputField = injectStore("addTask")(({ addTask }) => {
  const [text, updateText] = useState("");

  const submit = () => {
    addTask(text);
    updateText("");
  };

  return (
    <div>
      <input
        type="text"
        placeholder="Add New Task..."
        value={text}
        onChange={e => updateText(e.target.value)}
      />
      <button onClick={submit}>Add</button>
    </div>
  );
});

Since we are using the values returned by hooks, all the components are already subscribed to the state changes and the new task will be reflected in UI without any complex stuff 👍

TasksList

We now have to use the tasks property to display the list of tasks. For toggle option we just have to call toggleTask method with the task index. To pass multiple props from our useToDoList hook, we have to use an array in the injectStore HOC as follows.

const TasksList = injectStore(["tasks", "toggleTask"])(
  ({ tasks, toggleTask }) => {
    return (
      <ul>
        {tasks.map((item, itemIndex) => {
          const toggle = () => toggleTask(itemIndex);
          return (
            <li key={itemIndex}>
              <input
                type="checkbox"
                onChange={toggle}
                checked={item.isComplete}
              />
              {item.task}
            </li>
          );
        })}
      </ul>
    );
  }
);

TasksStats

You probably already know how to build this one 😎

const TasksStats = injectStore(["tasksCount", "completedTasksCount"])(
  ({ tasksCount, completedTasksCount }) => {
    return (
      <div>{`Total: ${tasksCount}, Completed: ${completedTasksCount}`}</div>
    );
  }
);

That concludes this tutorial. The final working code is available for you to try out in CodeSandbox

You can also create multiple stores. Just rename RexProvider & injectStore properties of each store before you export them to other components 😁

Examples

Performance

Rex State's injectStore HOC returns a memoized component (using useMemo) which ensures the component is re-rendered only when the property that the component is dependent on is updated.

Consider you have a hook such as ﹣

const useInfo = () => {
  const [state, setState] = useRex({
    title: "My Title",
    description: "Some Description"
  });

  return {
    get title() {
      return state.title;
    },
    get description() {
      return state.description;
    },
    updateDescription(newDescription) {
      setState({ description: newDescription });
    }
  };
};

A component that is dependent on the title alone will should not be affected by the updates to the description state using updateDescription method. injectStore ensures such re-renders are prevented.

However, this optimization is only applicable to primitive data types since non-primitive data types will not pass the javascript equality comparisons.

This means the following code is efficent 👍﹣

return {
  get title() {
    return state.title;
  },
  get description() {
    return state.description;
  }
};

The following code is inefficient 🙅 ﹣

return {
  get data() {
    return {
      title: state.title,
      description: state.description
    };
  }
};

This optimization is implemented based on the second option of Dan Abramov's suggestion.

useStore is basically a direct implementation of useContext without any such optimization. You can refer this example on how to use useStore and you can apply a different optimization ⚡️ if you want! ✨

Why Rex State?

  • It's Tiny!
  • Simple & un-opinionated
  • As fast as React
  • Built for projects of all sizes!

TODO:

  • Unit Tests
  • Performance Testing
  • CI/CD Setup
  • Fix Types ☹️

Licenses

MIT © DaniAkash

2.0.0-alpha-1

5 years ago

1.0.0

6 years ago

0.0.7-beta-3

6 years ago

0.0.7-beta-2

6 years ago

0.0.7-beta-1

6 years ago

0.0.7-alpha-2

6 years ago

0.0.7-alpha-1

6 years ago

0.0.6

6 years ago

0.0.6-alpha-5

6 years ago

0.0.6-alpha-4

6 years ago

0.0.6-alpha-3

6 years ago

0.0.6-alpha-2

6 years ago

0.0.6-alpha-1

6 years ago

0.0.6-alpha

6 years ago

0.0.4-beta-9

6 years ago

0.0.5

6 years ago

0.0.4-beta-7

6 years ago

0.0.4-beta-5

6 years ago

0.0.4-beta-4

6 years ago

0.0.4-beta-3

6 years ago

0.0.4-beta-2

6 years ago

0.0.4-beta-1

6 years ago

0.0.4-beta-0

6 years ago

0.0.4

6 years ago

0.0.3

6 years ago

0.0.2

6 years ago

0.0.1

6 years ago