Giter Club home page Giter Club logo

reactn's Introduction

ReactN

Tweet version minzipped size downloads

ReactN is an extension of React that includes global state management. It treats global state as if it were built into React itself -- without the boilerplate of third party libraries.

banner

For support, reach out to us on the Reactiflux Discord channel #reactn.

๐Ÿ’— this project? Become a sponsor.

Install

  • npm install reactn or
  • yarn add reactn

Features

No boilerplate

For function components, import { useGlobal } from "reactn"; to harness the power of React Hooks!

For class components, simply change import React from "react"; to import React from "reactn";, and your React class components will have global state built in!

If you prefer class decorators, you can continue to import React from "react"; for your components and additionally import reactn from "reactn"; for access to the @reactn decorator!

Intuitive

Function components

Global state in function components behaves almost identically to local state.

You use [ global, setGlobal ] = useGlobal() to access the entire global state object.

You use [ value, setValue ] = useGlobal(property) where property is the property of the global state you want to get and set.

Global reducers in function components behaves almost identically to local reducers.

You use dispatch = useDispatch(reducerFunction) to mimic the behavior of useReducer, where instead of providing an initial state, the state of the reducer is the ReactN global state object.

You use dispatch = useDispatch(reducerName) to use a reducer that was added by the addReducer helper function.

You use dispatch = useDispatch(reducerFunction, property) or [ value, dispatch ] = useDispatch(reducerFunction, property) to apply a reducer specifically to a global state property. This is very similar to React's native useReducer functionality.

Class components

Global state in class components behaves exactly like local state!

You use this.global and this.setGlobal to get and set the global state.

You use this.dispatch.reducerName() to dispatch to a reducer that was added by the addReducer helper function.

The @reactn decorator allows you to convert classes that extend React.Component to ReactN global state components.

Map state to props

If you prefer Redux's connect functionality, pure functions, or are dealing with deeply nested objects, a withGlobal higher-order component is also available.

Table of contents

Getting started

Managing multiple states

This README is for managing a single global state. This is ideal for most applications. If you are using concurrent server-side rendering or otherwise want to work with multiple global states, follow the README for the Provider component, which allows you to limit a ReactN state to a React Context.

If you are unsure whether or not you need multiple global states, then you do not need multiple global states.

Initializing your state

You can initialize your global state using the setGlobal helper function. In most cases, you do not want to initialize your global state in a component lifecycle method, as the global state should exist before your components attempt to render.

It is recommended that you initialize the global state just prior to mounting with ReactDOM.

import React, { setGlobal } from 'reactn';
import ReactDOM from 'react-dom';
import App from './App';

// Set an initial global state directly:
setGlobal({
  cards: [],
  disabled: false,
  initial: 'values',
  x: 1,
});

ReactDOM.render(<App />, document.getElementById('root'));

TypeScript support

ReactN supports TypeScript out of the box! It is written entirely in TypeScript. This gives it powerful intellisense, auto-complete, and error-catching abilities.

TypeScript can maintain inferred global state and reducer shape of a Providers. Unfortunately, without your help, it cannot track the shape of the "default" global state -- the one manipulated by the setGlobal and addReducer helper functions.

In order to tell TypeScript the shape of your global state when you are not using a Provider, create a file at src/global.d.ts with the following contents:

import "reactn";

declare module "reactn/default" {

  export interface Reducers {

    append: (
      global: State,
      dispatch: Dispatch,
      ...strings: any[]
    ) => Pick<State, "value">;

    increment: (
      global: State,
      dispatch: Dispatch,
      i: number,
    ) => Pick<State, "count">;

    doNothing: (
      global: State,
      dispatch: Dispatch,
    ) => null;
  }

  export interface State {
    count: number;
    value: string;
  }
}

In the above file, we extend the Reducers and State interfaces in the "reactn/default" file. While you will never use "reactn/default" in your code, ReactN will use it to determine the shape of the default global state.

The above example will add append, increment, and doNothing to your useDispatch and this.dispatch auto-completion and typing. The parameters and return values will also be correctly typed. In addition, it will also add count and value to your useGlobal and this.global auto-competion with the appropriate types as well.

Developer tools

ReactN is compatible with the Redux DevTools extension.

  • Install the Redux DevTools extension to your browser or environment.
  • Install the redux package to your project via npm or yarn. This is used to create a middleware Redux store for the Redux DevTools extension.
    • You do not have to import or use the redux package anywhere in your project.
    • You do not need to create a Redux store, reducer, or actions.
    • redux is just a peer dependency. It will be managed automatically.
  • Follow the instructions on the ReactN DevTools README.

Examples

Class components

By importing React from reactn instead of react, you bake global state directly into the React namespace. As a result, Component and PureComponent will have access to the global and dispatch member variables and setGlobal method.

import React from 'reactn'; // <-- reactn
import Card from '../card/card';

// Render all cards in the global state.
export default class Cards extends React.PureComponent {
  componentDidMount() {
    // Hydrate the global state with the response from /api/cards.
    this.setGlobal(
      // Despite fetch returning a Promise, ReactN can handle it.
      fetch('/api/cards')
        .then((response) => response.json())

        // Set the global `cards` property to the response.
        .then((cards) => ({ cards }))

        // Fail gracefully, set the global `error`
        //   property to the caught error.
        .catch((err) => ({ error: err }))
    );
  }

  render() {
    // For each card in the global state, render a Card component.
    // this.global returns the global state,
    //   much the same way this.state returns the local state.
    return (
      <div>
        {this.global.cards.map((card) => (
          <Card key={card.id} {...card} />
        ))}
      </div>
    );
  }
}

Class components (with decorator)

By importing React and ReactN separately, the React namespace remains unchanged. You can inject ReactN's global functionality into your vanilla React component by using the @reactn decorator imported from the reactn package.

import React from 'react';
import reactn from 'reactn'; // <-- reactn
import Card from '../card/card';

// Render all cards in the global state.
@reactn
export default class Cards extends React.PureComponent {
  componentDidMount() {
    // Hydrate the global state with the response from /api/cards.
    this.setGlobal(
      // Despite fetch returning a Promise, ReactN can handle it.
      fetch('/api/cards')
        .then((response) => response.json())

        // Set the global `cards` property to the response.
        .then((cards) => ({ cards }))

        // Fail gracefully, set the global `error`
        //   property to the caught error.
        .catch((err) => ({ error: err }))
    );
  }

  render() {
    // For each card in the global state, render a Card component.
    // this.global returns the global state,
    //   much the same way this.state returns the local state.
    return (
      <div>
        {this.global.cards.map((card) => (
          <Card key={card.id} {...card} />
        ))}
      </div>
    );
  }
}

Function components

Using React Hooks, you can harness useGlobal to access the global state.

import React, { useGlobal } from 'reactn'; // <-- reactn
import Card from '../card/card';

// Render all cards in the global state.
const Cards = () => {
  // Use the hook to get all cards in the global state.
  //   setCards is not used in this example.
  const [cards, setCards] = useGlobal('cards');

  // For each card in the global state, render a Card component.
  return (
    <div>
      {cards.map((card) => (
        <Card key={card.id} {...card} />
      ))}
    </div>
  );
};

export default Cards;

You may also use the useDispatch hook analogously to the useReducer hook by providing a function to useDispatch.

import React, { useDispatch } from 'reactn'; // <-- reactn

const incrementReducer = (global, dispatch, action) => ({
  count: global.count + action.amount,
});

const decrementReducer = (global, dispatch, action) => ({
  count: global.count - action.amount,
});

const MyComponent = () => {
  const increment = useDispatch(incrementReducer);
  const decrement = useDispatch(decrementReducer);

  return (
    <div>
      <button onClick={() => increment({ amount: 1 })}>Add 1</button>
      <button onClick={() => increment({ amount: 3 })}>Add 3</button>
      <button onClick={() => decrement({ amount: 5 })}>Subtract 5</button>
    </div>
  );
};

export default MyComponent;

By providing a second parameter to useDispatch that is the key of the global state, the return value of that reducer will set that property of the global state. This allows you to write your reducers similar to React's useReducer.

import React, { useDispatch } from 'reactn'; // <-- reactn

const incrementReducer = (count, action) => count + action.amount;

const decrementReducer = (count, action) => count - action.amount;

const MyComponent = () => {
  const increment = useDispatch(incrementReducer, 'count');
  const decrement = useDispatch(decrementReducer, 'count');

  return (
    <div>
      <button onClick={() => increment({ amount: 1 })}>Add 1</button>
      <button onClick={() => increment({ amount: 3 })}>Add 3</button>
      <button onClick={() => decrement({ amount: 5 })}>Subtract 5</button>
    </div>
  );
};

export default MyComponent;

Helper functions

addCallback

Use addCallback to execute a function whenever the state changes. The return value of the callback will update the global state, so be sure to only return undefined or null if you do not want the global state to change. Be aware that always returning a new state value will result in an infinite loop, as the new global state will trigger the very same callback.

The only parameter is the callback function.

import { addCallback, setGlobal } from 'reactn';

// Every time the global state changes, this function will execute.
addCallback((global, dispatcherMap, stateChange) => {
  alert(`The new value is ${global.value}!`);

  // If the global state was changed to 1, change it to 2.
  if (global.value === 1) {
    return { value: 2 };
  }

  // If the global state is anything other than 1, don't change it.
  return null;
});

setGlobal({ value: 1 });
// The new value is 1!
// The new value is 2!

The return value of addCallback is a function that, when executed, removes the callback.

import { addCallback, setGlobal } from 'reactn';

const removeAlert = addCallback((global) => {
  alert(global.value);
});

// The callback causes an alert on global state change:
setGlobal({ value: 1 }); // 1
setGlobal({ value: 2 }); // 2

// No longer execute the callback.
removeAlert();

// No alerts:
setGlobal({ value: 3 });
setGlobal({ value: 4 });
addReducer

Use addReducer to add a reducer to your global state.

The first parameter is the name of your reducer. You will access your reducer by this name. this.dispatch.reducerName or useDispatch("reducerName").

The second parameter is the reducer function. The reducer function that you write has at least two parameters: first, the global state; second, a map of your reducers. The third and onward parameters are the arguments that you pass when dispatching. The reducer function that you use when dispatching does not contain the global state or map of reducers. Those are prefixed for you automatically.

import { addReducer, setGlobal, useDispatch, useGlobal } from 'reactn';

// Initialize the global state with the value 0.
setGlobal({ value: 0 });

// When the increment reducer is called, increment the global value by X.
addReducer('increment', (global, dispatch, x = 1) => ({
  value: global.value + x,
}));

function MyComponent() {
  const increment = useDispatch('increment');
  const [value] = useGlobal('value');
  return (
    <>
      The value is{' '}
      <button
        onClick={() => {
          // Increment from 0 to 1.
          // (the default value of the reducer is 1)
          if (value === 0) {
            increment();
          }

          // Increment from 1 to 5.
          else if (value === 1) {
            increment(4);
          }
        }}
        value={value}
      />
    </>
  );
}

For a class component, the analogous method is this.dispatch.increment(value).

The dispatch parameter on a reducer allows you to write "sagas," or a single reducer that dispatches other reducers.

// add(1)
addReducer('add', (global, dispatch, i) => ({
  x: global.x + i,
}));

// subtract(2)
addReducer('subtract', (global, dispatch, i) => ({
  x: global.x - i,
}));

// addSubtract(1, 2)
addReducer('addSubtract', async (global, dispatch, i, j) => {
  await dispatch.add(i);
  await dispatch.subtract(j);
});
addReducers

addReducers accepts an object where the keys are reducer names and the values are reducers. addReducers is just a convenient shorthand for calling addReducer multiple times.

getDispatch

Use getDispatch to return an object of the global dispatch functions. You only want to use this in helper libraries, and not in Components. Components should use useDispatch or this.dispatch.

getDispatch has no parameters.

import { getDispatch } from 'reactn';

// Access this.dispatch.reducerName outside of a Component.
class HelperLibrary {
  getDispatcherFunction() {
    return getDispatch().reducerName;
  }
}
getGlobal

Use getGlobal to return a current snapshot of the global state. You only want to use this in helper libraries, and not in Components. Components should use useGlobal or this.global to ensure that they re-render when the global state changes. getGlobal will not cause a Component reliant on the global state to re-render, nor will it cause a library function to re-execute. It does nothing more than return a current snapshot of the global state.

getGlobal has no parameters.

import { getGlobal } from 'reactn';

// Access this.global.value outside of a Component.
class HelperLibrary {
  getGlobalValue() {
    return getGlobal().value;
  }
}
removeCallback

Use removeCallback to remove a callback that was added via addCallback. The callback must be the same function reference. This is equivalent to executing the return value of addCallback.

The only parameter is the callback function itself.

import { addCallback, removeCallback, setGlobal } from 'reactn';

function alertCallback(global) {
  alert(global.value);
}

addCallback(alertCallback);

// Alerts the global state value:
setGlobal({ value: 1 }); // 1
setGlobal({ value: 2 }); // 2

// Remove the alert callback:
removeCallback(alertCallback);

// No alerts:
setGlobal({ value: 3 });
setGlobal({ value: 4 });
resetGlobal

Use resetGlobal to reset the global state. This resets all state values, including callbacks, property listeners, and reducers.

There are no parameters.

import { getGlobal, resetGlobal, setGlobal } from 'reactn';

// Set the value.
setGlobal({ value: 1 });

// Get the value.
alert(getGlobal().value); // 1

// Reset the global state.
resetGlobal();

// Get the value.
alert(getGlobal().value); // undefined
setGlobal

Use setGlobal to initialize or update your global state. This is analogous to calling this.setGlobal in a class component or useGlobal()[1] in a function component.

The first parameter is merged into the global state in the same way a class component's this.setGlobal merges its first parameter into the local state.

The optional second parameter is a callback.

setGlobal with a new global state:

import { setGlobal } from 'reactn';

// Set loading to true.
setGlobal({
  loading: true,
});

setGlobal with a new global state and a callback:

import { setGlobal } from 'reactn';

// Set loading to true.
setGlobal(
  {
    loading: true,
  },

  // After it is set, assert that loading is true.
  (global) => {
    assert(global.loading === true);
  }
);
useDispatch

Requires React >= 16.8.0

The useDispatch helper function is a React Hook analogous to the useReducer hook built into React itself. useDispatch will dispatch a global reducer that has been added to ReactN via the addReducer, addReducers, or withInit helper functions or a global reducer that you specify inline as a parameter.

useDispatch()

useDispatch() with no parameters will return a map of all of your global reducers.

import { useDispatch } from 'reactn';

function MyComponent() {
  const dispatch = useDispatch();
  dispatch.add(1);
  dispatch.substract(2);
  return null;
}
useDispatch(Function)

useDispatch(f) allows you to define your global reducer inline. This method is particularly useful if you prefer to import your reducers as needed or keep your singleton reducers with the components that use them.

import React, { useDispatch, useGlobal } from 'reactn';

function MyComponent() {
  const [count] = useGlobal('count');
  const add = useDispatch((global, _dispatch, n) => ({
    count: global.count + n,
  }));
  return <button onClick={() => add(1)}>{count}.</span>;
}
useDispatch(Function, keyof State)

useDispatch(f, "property") allows you to define your global property reducer inline. A property reducer changes only one property of the global state, which can greatly simplify your reducer logic.

import React, { useDispatch, useGlobal } from 'reactn';

function MyComponent() {
  const [count] = useGlobal('count');
  const add = useDispatch((count, n) => count + n, 'count');
  return <button onClick={() => add(1)}>{count}.</span>;
}
useDispatch(keyof Reducers)

useDispatch("reducerName") allows you to dispatch a global reducer.

import React, { useDispatch, useGlobal } from 'reactn';

function MyComponent() {
  const [count] = useGlobal('count');
  const add = useDispatch('add');
  return <button onClick={() => add(1)}>{count}.</span>;
}
useGlobal

Requires React >= 16.8.0

useGlobal is a React Hook analogous to the useState Hook built into React itself. useGlobal returns the global state or parts thereof.

useGlobal()

useGlobal() with no parameters will return the entire global state object and a function for changing properties of the global state.

The setGlobal function returned by useGlobal is analogous to the setGlobal helper function and this.setGlobal class method.

import React, { useGlobal } from "reactn";

function MyComponent() {
  const [ global, setGlobal ] = useGlobal();
  const generateNumber = () => {
    setGlobal(g => ({
      generations: g.generations + 1,
      myNumber: Math.floor(Math.random() * 100),
    });
  };
  return (
    <button onClick={generateNumber}>
      #{global.generations}: {global.myNumber}
    </button>
  );
}
useGlobal(keyof State)

useGlobal("property") returns a specific global state property and a function for updating that property.

import React, { useGlobal } from 'reactn';

const getRandomNumber = () => Math.floor(Math.random() * 100);

function MyComponent() {
  const [myNumber, setMyNumber] = useGlobal('myNumber');
  return <button onClick={() => setMyNumber(getRandomNumber())}>{myNumber}</button>;
}
withGlobal

Use withGlobal to return a higher-order component to convert global state values into props. This is highly analogous to react-redux's connect function.

The first parameter is a function for getting global state values.

The second parameter is a function for setting global state values (similar to dispatch).

import React, { withGlobal } from 'reactn';

// A button that displays the value and, when clicked, increments it.
function MyComponent(props) {
  return (
    <>
      My value is <button onClick={props.incrementValue} value={props.value} />
    </>
  );
}

export default withGlobal(
  // Set the `value` prop equal to the global state's `value` property.
  (global) => ({
    value: global.value,
  }),

  // Important Note: This is not the setGlobal helper function.
  // Set the `incrementValue` prop to a function that increments the global
  //   state's `value` property.
  (setGlobal) => ({
    incrementValue: () => {
      // Important Note: This is not the setGlobal helper function.
      // This is the parameter referenced 4 lines up.
      setGlobal((global) => ({
        value: global.value + 1,
      }));
    },
  })
)(MyComponent);
withInit

In some cases (such as when using Next.js), you may be unable to run a setup script prior to your ReactN components mounting. In order to instantiate your global state and reducers prior to mounting, you may use the withInit Higher Order Component. This HOC will await the setting of your global state before mounting the provided Lower Order Component (e.g. <App />).

import React, { useDispatch, useGlobal, withInit } from 'reactn';

const INITIAL_REDUCERS = {
  addOne: ({ count }) => ({
    count: count + 1,
  }),
};

const INITIAL_STATE = {
  count: 0,
};

export default withInit(
  INITIAL_STATE,
  INITIAL_REDUCERS
)(function App() {
  const addOne = useDispatch('addOne');
  const [count] = useGlobal('count');
  return <button onClick={addOne}>Count: {count}</button>;
});

Known issues

  • super(props) is incompatible with TypeScript. #126
  • Components re-render once per changed subscribed property. #129
  • Class components use componentWillUpdate without the UNSAFE_ prefix. #134
  • Class components are incompatible with Providers in newer versions of React. #132

Terminology

ReactN strictly maintains accurate terminology for its data structures. The majority of ReactN's data structures are meant to be black box to simplify the user experience, only referenced by name in the package's code. They are outlined here for transparency and to ease community contributions.

Dispatcher

When you pass a reducer to ReactN via addReducer, addReducers, useDispatch, or withInit, ReactN returns a dispatcher.

A dispatcher is a function that wraps a reducer, passing the global state and global reducers as parameters tying its return value to the global state. Dispatchers and reducers have a 1-to-1 relationship and are tightly bound to each other.

In documentation, dispatchers are often referred to as reducers to decrease the cognitive overhead and conceptually strengthen their 1-to-1 relationship.

For example, an "add" reducer may be defined as follows:

function add(global, _dispatch, n) {
  return { count: global.count + n };
}

When you call this reducer, you only need to call add(1). This difference in call signature is because you are calling the dispatcher.

A dispatcher, in pseudo-code, conceptually looks as follows:

function dispatchAdd(n) {
  const { dispatchers, set, state } = globalStateManager;
  const newGlobalState = add(state, dispatchers, n);
  return set(newGlobalState);
}

Global state manager

The global state manager is the core object that powers ReactN. It maintains the state, global dispatchers, and subscriptions.

Default global state manager

The default global state manager is the global state manager used by all of ReactN unless otherwise specified. To specify a different global state manager, you must use a Provider.

ReactN Components and Hooks will attempt to find a global state manager via the Context. If one does not exist via Context, it will fallback to the default global state manager.

Reducer

A reducer is a function that accepts the current global state, a map of all global reducers, and any number of additional parameters. A reducer returns a change to the global state. It does not need to return the entire new global state. It only needs to return key-value pairs of changed properties.

An example "add" reducer may be defined as follows:

function add(global, _dispatch, n) {
  return { count: global.count + n };
}

A reducer may be asynchronous (return a Promise) and asynchronously dispatch other reducers. You can use a reducer that dispatches other reducers to create a "saga" of state changes.

async function mySaga(global, dispatch, shouldMultiply) {
  if (global.count < 0) {
    await dispatch.add(1);
  }
  await dispatch.subtract(2);
  if (shouldMultiply) {
    await dispatch.multiply(3);
  }
}

mySaga(true); // shouldMultiply = true

Property reducer

A property reducer is a reducer that only changes one property. They only receive that property's value as a parameter instead of the entire global state object, and they do not receive the dispatch object as a parameter at all.

An example "add" property reducer may be defined as follows:

function add(count, n) {
  return count + n;
}

You must specify the property when using a property reducer. Property reducers cannot be added to or remembered by the global state manager.

import React, { useDispatch, useGlobal } from 'reactn';

function add(count, n) {
  return count + n;
}

function MyComponent() {
  const [count] = useGlobal('count');
  // Use the "add" property reducer on the "count" property.
  const dispatch = useDispatch(add, 'count');
  return <button onClick={() => dispatch(1)}>{count}</button>;
}

Sponsor

If you are a fan of this project, you may become a sponsor via GitHub's Sponsors Program.

Support

For support, reach out to us on the Reactiflux Discord channel #reactn.

chat

reactn's People

Contributors

bdq avatar bravo-kernel avatar dependabot-preview[bot] avatar dependabot[bot] avatar jiangweixian avatar olederle avatar quisido avatar r1ckyrockz avatar resistdesign avatar rolandzwaga avatar siapdev avatar thejoebiz avatar trevorsayre avatar umbertoghio avatar ysm-dev avatar yuritoledo avatar

Stargazers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

Watchers

 avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar  avatar

reactn's Issues

Safe to use setGlobal anywhere?

Hey, really cool library.

I have a quick question though. Is using setGlobal inside components safe? Specifically functional components. I'm using the following pattern for instance:

import React from 'react';
import { setGlobal, useGlobal } from 'reactn';

interface InputProps {
  stateKey: string; // state identifier
  label: string;
}

export default (props: InputProps) => {
  const [value, setValue] = useGlobal(props.stateKey);
  if (value === undefined) {
    setGlobal({ [props.stateKey]: '' });
  }

  return (
    <div>
      <div>{props.label}</div>
      <input
        value={value}
        onChange={e => {
          e.preventDefault();
          setValue(e.target.value);
        }}
      />
    </div>
  );
};

based off of the example code that uses classes and calls setGlobal in componentDidMount.

Finally, I tried calling setGlobal inside of useEffect to no effect hence the need for wrapping setGlobal in an if statement. Is this a bug or intentional?

Expired cert

Not sure if this is the appropriate place to report but the cert on https://reactn.com has expired:

NET::ERR_CERT_DATE_INVALID
Subject: *.reactn.com
Issuer: COMODO RSA Domain Validation Secure Server CA
Expires on: Sep 28, 2018
Current date: Nov 19, 2018

Typescript Support

Hi, not really an issue but rather a feature request: loving Reactn, I've started using it in my projects, any plans to make it typescript ready?

Implement global state life cycle methods.

Hi,

I have experiment this behavior :

  • I use this.global.x in componentDidUpdate in component A
  • When component B updates global.x, component A doesn't rerender
  • I make something else to make component A render
  • Then when component B updates global.x it does make component A render

I suppose that before the first call of componentDidUpdate, we don't know that component A is using global.x, so it is not rerendered until something make it call componenetDidUpdate, at this time we see that component A uses global.x.

I am not sure my issue is clear, is it the expected behavior or do you think it can be changed ?

Thank you

addReducer helper causes components to re-render unnecessarily

Hi,

Thank you for this great library, it really shorten the code.

I have noticed that my components using this.global render whenever global is updated, even if it updates something not used by this component. Is it normal ?
I was expecting a behavior like React.PureComponent, the component just renders when the part of the state he uses get rendered, is it possible ?

Especially in my case, I have a component that uses a reducer function, it doesn't use a value of the state.
I am not on react 16.7 so I cannot use useGlobal.

PS: I am on react 16.6.

Thank you

Document that setting a previously undefined property does not trigger a re-render.

Hi

I just stumbled upon a small "nice-to-know".

If using useGlobal() in a simple (functional/stateless) component, the global state settings your are accessing must be initalized prior to the call to useGlobal(). This might be obvious to some, but I was wondering why my component would not re-render itself when updating the global, but as soon as I made sure to initialize the global beforehand, it started working.

Apparently this is not mentioned in the documentation, but if it is, then my apologies.

[TypeScript] Decorator should return ReactN.Component

@reactn
class X extends React.Component {
  render() {
    this.global; // property does not exist (it does)
  }
}

This is a TypeScript error, not a runtime error.
Tests pass with @ts-ignore on "property does not exist" errors.

Autocompletion

Hi, is there a way to get autocompletion of my store?

I am doing>

interface Store {
  fahrenheit: number
}

export const store: Store = {
  fahrenheit: 79
}
...
setGlobal(store)

then

import React, { useGlobal } from "reactn"

export default () => {
  const [store, updateStore] = useGlobal()
  const onColder = () => {
    updateStore({ fahrenheit: store.fahrenheit - 1 })
  }
...

I was expecting by typing store. the autompletion would kick in..

Migrating from https://github.com/gunn/pure-store/blob/master/src/index.ts
where this works great.

withGlobal HOC

A withGlobal HOC that behaves similar to Redux's connect may offer an optional interface intuitive to Redux users. This would allow for components to remain pure and may circumvent the need to add global state key listeners on nested keys (#4):

withGlobal(global => ({
  propName: global.x.y
}))(Component);

While the HOC re-renders when global.x changes, the Component will not if it is a PureComponent.

Setting an undefined value does not trigger a re-render.

Maybe it's just me being stupid, but I had a hard time figuring out why my components did not update (with no errors or warnings) when I changed the global state. Turne out I initially had set the global state like this:

setGlobal({
  status: undefined,
  data: {},
})

Changes to the "status" item did not trigger any component updates. Solved by changing the initial state to

setGlobal({
  status: false, // null also works
  data: {},
})

A small comment about this in the docs? Or maybe it's totally obvious to everyone else :-)

Quick question about "How do components update?" in FAQs

If I store an array of objects in Global state, and map over it to render each object in a list with keys. Does ReactN trigger re-render of the entire list of components if I append a new object to the list or change one of the objects?

Warning in Unmounted Component (react-router)

Hi there, I currently have a Login Component, and based on the server's response I will call (react-router-dom) <Redirect to='/dashboard' /> if the server responds with affirmation the user has a valid session cookie already. However, I get this warning inside my component:

Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
    in Login (created by LoginBox)
    in div (created by Card)
    in div (created by Card)
    in Card (created by LoginBox)
    in LoginBox (created by Route)

To clarify, the setState is already done by the time the redirect is called:

// Hooks, Handlers, etc.
...
if (authorized) {
        return <Redirect to='/dashboard' />
}
// else, Display Login Component
...

From the Warning, I get a hint that something should be cleaned up? Not sure if this is related to #5.

Spread operating reducer state includes reducers in the state.

addReducer('name', global => ({
  ...global, // reducers are now in the state
  newKey: 'new value'
}));

Best practice is to not spread operate the root, because there is no need to. This may not be intuitive to Redux users, so I'd like to support spread operating as a "backwards compatible" syntax, despite it being unnecessary.

I am pretty sold on the idea of having reducers available inside reducers.

Current fix is to silently ignore attempts to set state properties that share a name with reducers.

Support React versions < 16.3.0

Throws error at Index.js (line 105):

"TypeError: object(...) is not a function"

when importing reactn in react version 16.0.0. When upgrading react to newest version (16.8.6), I was able to import without issues. Took me quite some time to figure it out since it says in your FAQ that reactn supports all react versions across the board. Apparently that's not the case.

[TypeScript] Add React properties

Currently only ReactN properties are importable: decorator, addReducer, setGlobal, Component, PureComponent, etc.

React properties should be importable as well: createElement (though this doesn't break JSX for some reason?), useEffect, useState

In index.d.ts, we have:

interface ReactN {
  importableProperty: Etc;
}

It needs to be:

interface ReactN extends React {
  importableProperty: Etc;
}

However, this throws Cannot use namespace 'React' as a type, since React is not an object but a namespace.

Unsure how to fix.

Support non-object global states.

Something that would help with typescript/flow typings, as well as make it easier to use the package correctly - instead of hinting that global state should be initialised before ever calling useGlobal, you could be sure that setGlobal had been called, if that were the recommended/only way to get the useGlobal reference:

// state.js
import { setGlobal } from 'reactn';
export const useGlobal = setGlobal({
  cards: [],
  disabled: false,
  initial: 'values',
  x: 1
});
// index.js
import ReactDOM from 'react-dom';
import App from './App';

// no need to call `useGlobal` here:
// it'll be called when the module is `require(...)`d for the first time.
ReactDOM.render(
  <App />,
  document.getElementById('root')
);
// SomeComponent.js
import { useGlobal } from './state'
export const SomeComponent = () => {
  const [state, setGlobal] = useGlobal()
  return <div>{state.cards.join(', ')}</div>
}

In index.d.ts, setGlobal could be typed with a generic to so useGlobal could also have useful types (right now, { [property: string]: any } which allows any property access with no restrictions and no hints):

export function setGlobal<State>(initialState: State): UseGlobal<State>
interface UseGlobal<State> {
  (): UseGlobalResult<State>
  <Key extends keyof State>(key: Key): UseGlobalResult<State[Key]>
}
type UseGlobalResult<T> = [T, (newState: T) => void]

You then get meaningful autocomplete out-of-the-box. This would work even in javascript in most IDEs, because the type passed in to setGlobal can be inferred. Example:

image

Here the compiler finds the bug - that we're trying to pass in a number to cards, which is an array of strings.

One other possible benefit (although possibly not initially; it would depend on the implementation) is that by being more functional - returning a function rather than relying on side-effects - you could conceivably have multiple state objects that can span components.

What do you think? If you'd be open to the change I could help with a PR.

Document how to test a ReactN Component

Hi @CharlesStover ,

I'm experimenting reactn using useGlobal hook. I'm excited with the simplicity brought by reactn against redux!

One thing I missed is a general path of how to test things up. Reactn is too fresh and there isn't too much resources out there.

I started using the useGlobal hook, and I'm testing my components using mocks. I've wrote a mock file __mocks__/reactn.js:

export const useGlobal = jest.fn()

Suppose I have a component that apply the useGlobal like this:

const [something, setSomething] = useGlobal('something')

And suppose a correponding test SomeComponent.test.js like:

const something = 'some-value'
const setSomething = jest.fn()
useGlobal.mockImplementationOnce([something, setSomething])
const wrapper = shallow(<SomeComponent />)
wrapper.simulate('click')
expect(setSomething).toBeCalledWith('foo-bar')
expect(useGlobal).toBeCalledWith(['something']);
expect(useGlobal.mock.calls.map(call => call[0])).toEqual(['something']);

This can work, but I'm not sure if it is the best approach.

My question is: what do you do for test the reactn usage? Do you have any plan for test helpers? Do you see them as part of the reactn core?

Import woodoo

Haven't played with this yet but it definitely seems promising. One thing I either misunderstood or might be wrong design is the way you import these components. While just replacing 'react' with 'reactn' sounds rly cool and easy to use. What if somebody in the future releases a lib called e.g. 'reactm' also adding some functionality. What would you do to use both at the same time? I know there are some HOCs you could use, but this still feels a little shady to me.

CRA SSR : ReferenceError: window is not defined

Running in to this issue on SSR CRA and have not manage to get around it.

$ cross-env NODE_ENV=production node ./server/index.js
/Users/mathias/Dropbox/Projects/slight/rettfinans/node_modules/reactn/index.js:1
(function (exports, require, module, __filename, __dirname) { !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e(require("react")):"function"==typeof define&&define.amd?define("reactn",["react"],e):"object"==typeof exports?exports.reactn=e(require("react")):t.reactn=e(t.React)}(window,function(t){return function(t){var e={};function n(r){if(e[r])return e[r].exports;var o=e[r]={i:r,l:!1,exports:{}};return t[r].call(o.exports,o,o.exports,n),o.l=!0,o.exports}return n.m=t,n.c=e,n.d=function(t,e,r){n.o(t,e)||Object.defineProperty(t,e,{enumerable:!0,get:r})},n.r=function(t){"undefined"!=typeof Symbol&&Symbol.toStringTag&&Object.defineProperty(t,Symbol.toStringTag,{value:"Module"}),Object.defineProperty(t,"__esModule",{value:!0})},n.t=function(t,e){if(1&e&&(t=n(t)),8&e)return t;if(4&e&&"object"==typeof t&&t&&t.__esModule)return t;var r=Object.create(null);if(n.r(r),Object.defineProperty(r,"default",{enum

ReferenceError: window is not defined
    at Object.<anonymous> (/Users/mathias/Dropbox/Projects/slight/rettfinans/node_modules/reactn/index.js:1:249)
    at Module._compile (internal/modules/cjs/loader.js:688:30)
    at Module._compile (/Users/mathias/Dropbox/Projects/slight/rettfinans/node_modules/pirates/lib/index.js:83:24)
    at Module._extensions..js (internal/modules/cjs/loader.js:699:10)
    at Object.newLoader [as .js] (/Users/mathias/Dropbox/Projects/slight/rettfinans/node_modules/pirates/lib/index.js:88:7)
    at Module.load (internal/modules/cjs/loader.js:598:32)
    at tryModuleLoad (internal/modules/cjs/loader.js:537:12)
    at Function.Module._load (internal/modules/cjs/loader.js:529:3)
    at Module.require (internal/modules/cjs/loader.js:636:17)
    at require (internal/modules/cjs/helpers.js:20:18)
error Command failed with exit code 1.
info Visit https://yarnpkg.com/en/docs/cli/run for documentation about this command.

Document setGlobal's state merging behavior.

One thing I noticed is missing from the documentation is what the merging behavior of setGlobal is.

Does having a state of { foo: 'fooVal' } and doing setGlobal({ bar: 'barVal' }) get you { foo: 'fooVal', bar: 'barVal' } (what I'm guessing, since that would match how component state works), or { bar: 'barVal' } (the more obvious JavaScript answer).

It would also be helpful to have API docs for the setGlobal and hook setters functions, outlining the possible argument types and the exact behavior for each.

Why export React from package

I noticed in the examples that it's suggested to be used like

import React, { setGlobal } from 'reactn';

why? It seems simpler and less ๐Ÿค” to keep things vanilla and stick to typical react / import patterns, like

import React from 'react';
import { setGlobal } from 'reactn';

For more cautious library users this could be something of a red flag.

Improve documentation

There are assumptions that ReactN is an alternative to React -- that it is the React framework as opposed to a middleman between the component and the user-installed version of React. "What if I want to upgrade my React version?" Make it more obvious that this is a non-issue.

Mention that CRA is supported.

useGlobal should match useReducer when passed a function

Following the classic example presented in this article, but with typescript:

https://gist.githubusercontent.com/CharlesStover/285f95bba12cd9468276d766a87a3568/raw/c386149729ff7d17c16318563be6613baf933448/global-reducer.js

I am playing a little fast and loose with types here. I am unsure what to do with the LocalReducer type returned in this scenario.

Expected

useGlobal(reducer) returns types compatiable with [state, dispatch]

Actual

useGlobal(reducer) returns LocalReducer

Code

import { useGlobal } from "reactn";

const initialState = { count: 0 };
function reducer(state: any, action: { type: string }) {
  switch (action.type) {
    case "reset":
      return initialState;
    case "add":
      return { count: state.count + 1 };
    case "subtract":
      return { count: state.count - +1 };
    default:
      return state;
  }
}

function Counter({ initialCount = 0 }) {
  const [state, dispatch] = useGlobal(reducer);
  return (
    <>
      Count: {state.count}
      {/* and can dispatch certain events here */}
      <button onClick={() => dispatch({ type: "reset" })}>Reset</button>
      <button onClick={() => dispatch({ type: "increment" })}>+</button>
      <button onClick={() => dispatch({ type: "decrement" })}>-</button>
    </>
  );
}

Error

Type 'LocalReducer' is not an array type.ts(2461)
const dispatch: any

Add setGlobal to the withGlobal HOC

Suggestion by @justinhandley in another issue.

Right now, import { setGlobal } from 'reactn'; should work without problem, however it may be more intuitive to include setGlobal in the HOC, similar to how react-redux uses mapStateToProps as well as mapDispatchToProps.

Parent component not updating when using global state from child

I have made i simple reconstruction of what the problem is:

import React, {useState, useGlobal, setGlobal} from 'reactn';
import { render } from 'react-dom';

setGlobal({
  user: {},
});

function Child({ parentFunction }) {
  const [user, setUserData] = useGlobal('user');

  function populateUserObject() {
    // Set user data
    setUserData({
      name: "John doe",
      email: "[email protected]",
      phone: "29636063",
    });

    // Call parentFunction()
    parentFunction();
  }

  return (
    <div>
      <button onClick={populateUserObject}>Click me to populate global user object</button>
    </div>
  )
}

function Parent() {
  const [user, setUserData] = useGlobal('user');

  function parentFunction() {
    // It should output the user object, but it outputs the initial state!!
    console.log(user);
  }

  return (
    <div>
      <Child parentFunction={parentFunction} />
    </div>
  )
}

render(
  <Parent />,
  document.getElementById('test-reactn')
);

When clicking on the childs button, i would expect it to update the global state, and therefor output the user object with data when clicking the button.

The problem is, that it does not do that and returns the initial state, which is an empty object. Clicking the button again, the user object with data is now outputted.

Am i missing something or is this the expected behavior?

It is also worth mentioning, that i get the expected behavior, when using class components instead of functional!

Optional Provider

Should help with concurrent SSR requests.

Context needs to be backwards compatible. Add the constructor(..., context) alternative?
Make sure createContext doesn't crash if older version of React.

Maybe only provide a Provider property if the React version supports useable context in the first place.

Provide the relevant global state to the methods -- the one from the context, not the built-in instance if a context is provided.

Safe fallback if no context is provided.

Relevant docs:
https://reactjs.org/docs/legacy-context.html -
https://reactjs.org/docs/context.html#classcontexttype - 16.3+
useContext - 16.7+

Subscribe to deep-nested global state properties.

Object key usage tracking is working only one level down, thus would be a source of performance issues for a wide set of cases, normally manageably by Redux.

Take a look how it was made in react-easy-state. Unfortunately (to you), that a-year-old-library is much more usable than this brand-new

Prevent forceUpdate on components without a render method

Hi @CharlesStover, thanks for your great work.
I think it could be useful to prevent forceUpdate invocation on Components that doesn't have a render method.
Something like this seems to be enough:

function ReactNGlobalCallback(_this) {
	if (typeof _this['render'] === 'function') {
		_this.updater.enqueueForceUpdate(_this, null, 'forceUpdate');
	}
}

Nevertheless, I suppose there's a better way to handle this situation, provided that it makes sense to you.

Remove listeners before force updating

The re-render may not subscribe to the same global state keys than previous renders did.

if (this.global.x) {
  if (this.global.y) {
    return 'yes';
  }
  return 'no';
}
return 'maybe';

When x is true, the component subscribes to both x and y. When x is false, the component should only be subscribed to x (and not also y).

The removal cannot occur as a part of the key listener, because you must still be able to listen to multiple keys at the same time. this.global.x + this.global.y must listen to both. Only remove pre-render and do not modify during render.

Can't call forceUpdate on a component that is not yet mounted

Hi. Thanks for the library, I love the concept and ease of use.

I have probably an uncommon use case. I have in the dom tree a wrapper component to handle adding and removing of toast notifications. It works when I use context provider and consumers, but I want to be able to call the method for adding a toast with a single static method call from literary anywhere, without consumer wrappers, from all classes and functions, be it react component of not.

So I figured out I can extract the adding function to a static single instance class (ToastAPI) and have it share state (list of toasts) with the wrapper component using your global state. But when called, I'm getting the error in the title for some reason. :(

My question is, is there some check that prohibits me from calling setGlobal from an unmounted react component or any class/function outside of react?

Thanks. Hopefully I've explained it clear enough. I can elaborate if need be.

Add Redux DevTools support.

I ask myself how i could monitor state for example in chrome (like REDUX Tool).
Do you have any hints for that?
Great development also very easy for me as beginner ๐Ÿ‘

Global Providers should have hooks as static methods (Provider.useGlobal)

Current is
setGlobal({ cards: [], disabled: false, initial: 'values', x: 1 });

Is it possible to group these global state like the following for better control and code management?

setGlobal-A({ cards: [], disabled: false, initial: 'values', x: 1 });

and

setGlobal-B({ posts: [], blogger: 5 });

is it work in react native

Hello
I want to ask if it work or support react native ??
and if not work there are any way to make it ?
thanks

Error: React v16.7 or newer is required.

Error: React v16.7 or newer is required
at t.exports /reactn/index.js:1:12795

Issue occured after updating
^16.7.0-alpha.0 > ^16.7.0

Tested

  • Removing / Installing node_packages = ๐Ÿ‘Ž
  • Removing yarn lock file = ๐Ÿ‘Ž
  • Downgrading to '16.7.0-alpha.0' = ๐Ÿ‘Ž

So now am confused what the cause of this issue could be. Could be something locally for me even if i did multiple modules wipe / install runs.

Feedback after reading 'No-boilerplate global state management in React'

Hi there,

thanks for writing the great article and turning your thoughts into a library.

What I would like to see from a global state library is the following light-weight version:

Setting up global state

import { GlobalStateProvider } from 'reactn'

export default function MyRoot() {
  return (
    <GlobalStateProvider initialState={...} ...>...</GlobalStateProvider>
  )
}

Using the global state

import { useGlobal } from 'reactn'

export default function MyComponent() {
  const [x, setX] = useGlobal('y.x')
  const [globalState, setGlobalState] = useGlobal()
}

You could add support for reducers and callbacks on the GlobalStateProvider.

I understand that you would like to support the component world as well, but I recommend to put that into another package (or put this into reactn-light).

I hope this feedback helps you.

Trouble with state resetting

Hi!

I'm not sure if I'm using it correctly or not, but I'm having trouble with the global state resetting to default value in the middle of all the magic. Code below for reference.

// index.js
setGlobal({
    authenticatedUser: {
        accessToken: null,
        isUserSignedIn: false,
        error: null,
        user: null
    },
    modules: {
        items: [],
        isLoading: false,
        error: null
    }
});
// actions.js
import { addReducer, setGlobal } from 'reactn';
import environment from '../configuration/environment.js';

addReducer('fetchModules', (global) => {
    // when I do this, component using this is showing isLoading correctly
    setGlobal({
        modules: {
            ...global.modules,
            isLoading: true
        }
    });

    return fetch(`${environment.API_URL}/modules`, {
        headers: {
            'Content-Type': 'application/json',
            'Authorization': global.authenticatedUser.accessToken
        }
    })
        .then(res => res.json())
        .then(json => {
            console.log(global.modules.isLoading);
            // this log returns default value set in index.js (false) and isLoading is false i component
            return {
                modules: {
                    ...global.modules,
                    items: json.data
                }
            }
        })
});

Why make it a drop-in replacement?

Not an issue, but a question.

I understand that reactn is a drop-in replacement for React that adds the ability to have global state.

What is the reason to do things this way, as opposed to having it shipped as a module to be used alongside standard React?

Isn't it hard to have to keep the two in sync?

Thanks!

Recommend Projects

  • React photo React

    A declarative, efficient, and flexible JavaScript library for building user interfaces.

  • Vue.js photo Vue.js

    ๐Ÿ–– Vue.js is a progressive, incrementally-adoptable JavaScript framework for building UI on the web.

  • Typescript photo Typescript

    TypeScript is a superset of JavaScript that compiles to clean JavaScript output.

  • TensorFlow photo TensorFlow

    An Open Source Machine Learning Framework for Everyone

  • Django photo Django

    The Web framework for perfectionists with deadlines.

  • D3 photo D3

    Bring data to life with SVG, Canvas and HTML. ๐Ÿ“Š๐Ÿ“ˆ๐ŸŽ‰

Recommend Topics

  • javascript

    JavaScript (JS) is a lightweight interpreted programming language with first-class functions.

  • web

    Some thing interesting about web. New door for the world.

  • server

    A server is a program made to process requests and deliver data to clients.

  • Machine learning

    Machine learning is a way of modeling and interpreting data that allows a piece of software to respond intelligently.

  • Game

    Some thing interesting about game, make everyone happy.

Recommend Org

  • Facebook photo Facebook

    We are working to build community through open source technology. NB: members must have two-factor auth.

  • Microsoft photo Microsoft

    Open source projects and samples from Microsoft.

  • Google photo Google

    Google โค๏ธ Open Source for everyone.

  • D3 photo D3

    Data-Driven Documents codes.