/react-simple-state

A simple and predictable state management for React and React Native Applications.

Primary LanguageTypeScript

React Simple State

A simple and predictable state management for React and React Native Applications.

Goals:

  • Easy to use and to maintain state management on a React Application.
  • Scalable
  • Easy to organize
  • Compatible with Hooks and Class based Components.

Inspired by:

Installation

  1. Run on your terminal the following command:
$ npm i --save @cobuildlab/react-simple-state
  1. To import the library anywhere you would like to use it:
import {
  createEvent,
  useSubscription,
  useEvent,
  View,
} from '@cobuildlab/react-simple-state';

API Docsw

Object Description
EventParams Params for the createEvent function.
createEvent Helper function to create state events.
View Subclass of React.View that includes a this.subscribe method to subscribe to changes on an Event.
useSubscription A hook for subscribe to specific events with a callback.
EventHookParams Params for the useEvent
useEvent A declarative alternative to the useSubscription.

EventParams

  • initialValue - An initial value for the event.
  • reducer A function that mutates the state before it gets propagated.

createEvent(params: EventParams)

  • Allows you to create a subscribable event.
  • The result object can be used for subscriptions with the View or the hooks: useEvent and useSubscription

Example

// agency-events.js
import { createEvent } from '@cobuildlab/react-simple-state';

export const OnAgencyList = createEvent();
export const OnAgencyListError = createEvent();
export const OnNewAgent = createEvent({
  initialValue: new Agent(),
  reducer: (prevState) => {
    prevState.agencies = OnAgencyList.get();
    return prevState;
  },
});

View

  • Allows you to create subscription to events from Class based components
  • It handles the subscription lifecycle: subscribe and unsubscribe on react lifecycle events.

Example

// AgencyView.js

import {View} from "@cobuildlab/react-simple-state";
import {OnAgencyList, OnNewAgent} from "./agency-events.js"

class AgencyView extend View{

  componentDidMount () {
    this.subscribe(OnAgencyList, (state)=> {
      // So something with the state.
    })

    this.subscribe(OnNewAgent, (state)=> {
      // So something with the state.
    })

  }
}

useSubscription(store, eventName, callback)

  • It subscribes a callback to an Event from functional components using hooks.
  • It handles the subscription lifecycle

Example

// AgencyView.js

import {useSubscription} from "@cobuildlab/react-simple-state";
import {OnAgencyList, OnNewAgent} from "./agency-events.js"

const AgencyView = ()=> {
    useSubscription(OnAgencyList, (state)=>{
        // Do something with the state
    });

    useSubscription(OnNewAgent, (state)=>{
        // Do something with the state
    });

    return ();
}

EventHookParams

  • initialValue - An initial value for the event.
  • reducer A function that mutates the state before it gets propagated.

useEvent(eventName, initialValue, reducer)

  • It subscribes ton event from functional components using hooks with a declarative approach.
  • It handles the subscription lifecycle
  • It handles initial values for the events
  • It handles a reducer function for the state.

Example

// AgencyView.js

import {useEvent} from "@cobuildlab/react-simple-state";
import {OnAgencyList, OnNewAgent} from "./agency-events.js"

const AgencyView = ()=> {
    const state = useEvent(OnAgencyList);
    const agent = useEvent(OnNewAgent, {initialValue: {}, reducer: (prevState) => prevState.agent});

    return ();
}

Full Example

Let's build a Flux Workflow for authentication

1) First, declare your Events

import { createEvent } from '@cobuildlab/react-simple-state';

export const LogoutEvent = createEvent({
  reducer: (prevState) => {
    localStorage.clear();
    return prevState;
  },
});
export const LoginEvent = createEvent();
export const PermissionError = createEvent({
  reducer: (prevState) => {
    LogoutEvent.dispatch();
    return prevState;
  },
});

export { LogoutEvent, LoginEvent, PermissionError };

2) Registering with the Store changes

import React from 'react';
import {LogoutEvent, LoginEvent,PermissionError } from "./agency-events.js";
import {View, useSubscription, useEvent} from '@cobuildlab/react-simple-state';

// Class Based
class View extends View {
      constructor(){
          super();
      }
      componentDidMount() {
          this.subscribe(LoginEvent, (state) => {
              // Do something useful with the Event Data
              const userName = state.user.name;
              this.setState({userName});
          });
          // Register some method
          this.subscribe(LogoutEvent this.logOutEvent);
      }

      logOutEvent = (state) => {
        //DO something with the state or the state of the Store
        const storeState = LogoutEvent.get();
      }
  }

// or Functional with React Hooks
const View = (props) => {
  // Set an Initial Value
  const loginState = useEvent(LoginEvent);
  const userState = useEvent(LogoutEvent, {reducer:(state) => state.user});

  useSubscription(LoginEvent, (state) => {
    // setError
    // toast.error()
  });

  return (
    {loginState && <User user={loginState}>}
  )
};

3) Define some actions that will trigger the event

import { LoginEvent, LogoutEvent } from './agency-events.js';

const authenticateAction = (username, password) => {
  if (username === undefined) return LogoutEvent.dispatch();

  let dataToSave = {
    authenticated: true,
    username,
    password,
  };
  LoginEvent.dispatch(dataToSave);
};

export { authenticateAction };

4) Actions can be created with createAction helper

import { createAction } from '@cobuildlab/react-simple-state';
import { OnFecthUserEvent, OnFetchUserErrorEvent } from './events';
import { apiClient } from './api';

// single declarition of the async service and the action
export const fetchUserAction = createAction(
  OnFecthUserEvent,
  OnFetchUserErrorEvent,
  async (id) => {
    const user = await apiClient.fetch({ user: id });

    return user;
  },
);

// Or we could declare the async service and then use in with diferent actions

export const fetchUserService = async (id) => {
  const user = await apiClient.fetch({ user: id });

  return user;
};

export const fetchMainUserAction = createAction(
  OnFecthMainUserEvent,
  OnFetchMainUserErrorEvent,
  fetchUserService,
);
export const fetchSecondaryUserAction = createAction(
  OnFecthSecondaryUserEvent,
  OnFetchSecondaryUserErrorEvent,
  fetchUserService,
);

5) Fetch can be done with useFetchAction hook

import { useFetchAction } from '@cobuildlab/react-simple-state';
import { fetchUser } from './actions';

// UserProfile component
export const UserProfile = ({ userId, loadingToken }) => {
  const [user, loadingUser] = useFetchAction(fetchUser, [userId]);

  // OR... skip the fetch ultil the token loads
  const [user, loadingUser] = useFetchAction(fetchUser, [userId], {
    skip: loadingToken,
  });
  // OR...

  const [user, loadingUser] = useFetchAction(fetchUser, [userId], {
    onCompleted: () => {
      toast.success('user fetched');
    },
    onError: () => {
      toast.error('Error when fetching user');
    },
  });

  // OR...

  const [user, loadingUser, { refetch }] = useFetchAction(fetchUser, [userId], {
    onCompleted: () => {
      toast.success('user fetched');
    },
    onError: () => {
      toast.error('Error when fetching user');
    },
  });

  useSubscription(OnSaveUserEvent, () => {
    // refetch the user after saving form for example
    refetch();
  });
  return; // profile view
};

6) callable fetch can be done with useCallAction hook

import { useCallAction } from '@cobuildlab/react-simple-state';
import { saveUser } from './actions';

// UserProfile component
export const UserProfile = ({ userId }) => {
  const userData = userDataState;
  const [save, loadingSubmit] = useCallAction(saveUser);
  // OR...

  // setup the action, and return a function that will trigger the action when it needed.
  const [save, loadingSubmit] = useCallAction(saveUser, {
    onCompleted: () => {
      toast.success('user saved');
    },
    onError: () => {
      toast.error('Error when saving user');
    },
  });

  return (
    <Form>
      <SubmitButton onClick={() => save(userId, useCallAction)} />
    </Form>
  );
};

Changelog

v0.6.0:

  • types improvements to be more genereic.

v0.5.0:

  • Add useCallAction and useFetchAction hooks to have a better declaritive way to handle promise in components.

v0.4.4:

  • Add isEmpty method to event to know if the event has data without to call the get method.

v0.4.0:

  • Add createAction decorator

v0.3.0:

  • Cache callback on useEvent
  • Cache the callback on useSubscription and add a dependencies parameter.
  • Remove Ramda and Rxjs as dependencies

v0.2.0:

  • Remove: receiveLastValue for the useQuery hook

v0.1.0:

  • Typos and documentation

v0.0.1:

  • State Draft