5.1.2 • Published 3 years ago

@smart-hooks/use-interstate v5.1.2

Weekly downloads
16
License
Unlicense
Repository
github
Last release
3 years ago

useInterstate


NEW. We added an enhanced interface allowing to obtain a setter along with the value of the state in the easiest way.

The support of TypeScript in the library is strong.


use-interstate

useInterstate is a simple, lightweight, and powerful global state management solution for React projects designed to stick as close as possible to a natural usage pattern found in the standard hook useState. It is minimalistic and does not require too much to start using. No special boilerplates. No big learning curve. The hook useInterstate is just working familiarly out-of-box. Also, it should work in the upcoming concurrent mode as we can judge assuming from information available now and based on our tries with the experimental build of React.

The library is written in TypeScript and nicely typed. The test coverage is solid and embraces various complex use cases. More than that, its name sounds cool.

Install

npm install @smart-hooks/use-interstate --save

Usage

There area two call interfaces for useInterstate.

useInterstate(key, initValue)

The first one is very close to the interface by which the standard React hook useState is managing the state.

import React from 'react';
import { useInterstate } from '@smart-hooks/use-interstate';

const STATE_KEY = 'STATE_KEY';

const Component1 = () => {
  const [useSubscribe, setInterstate] = useInterstate(STATE_KEY, '');
  const state = useSubscribe();

  return (
    <form>
      <label>
        Type a text:
        <input type="text" value={state} onChange={(e) => setInterstate(e.target.value)} />
      </label>
    </form>
  );
};

const Component2 = () => {
  const [useSubscribe] = useInterstate(STATE_KEY);
  const state = useSubscribe();

  return <p>Here is what you are typing: {state}</p>;
};

const InterstateExample = () => (
  <>
    <Component1 />
    <Component2 />
  </>
);

You must keep in mind two notable differences from useState.

  1. It needs to give a unique key to handle the state related to that key. It must be a number, string, or symbol. Of course, we guarantee that a mutable value linked to the key that constitutes the part of the global state will be the same in any part of an application at the same moment. The returned method provides the way how to pass changes to the global state. And any components that have subscribed to the pair key and value will be aware of the changes transmitted to the global state related to this key virtually with no delay.

  2. One more step is needed to subscribe to the state. Just compare:

    const [state, setState] = useState(initValue);

    and

    const [useSubscribe, setState] = useInterstate(key, initValue);
    const state = useSubscribe();

    useInterstate will not give a stored value just after calling the hook. Instead, it returns another hook that, being called, for its part returns a requested value of the state. The returned subscription function is considered a standard hook, so it must obey the rules of hooks from the very moment of birth. It may not be in a conditional clause, for example. Exactly this hook will listen to state changes and trigger re-rendering the component when the change occurs. Indeed, we broke up the subscribing process onto two separate steps because we aimed to create a tool that has a number of words in the vocabulary as small as possible but still gets a choice to opt-in subscribing on demand. Otherwise, as performing a subscription to the state in a body of a React component has its own performance cost you would get forced to assume it whenever using the hook even if the only thing you need is to have a setter to manipulate the state not listening to it.

    The default value passed to the hook can be a function. It will run only once, and its return will provide an actual initialization value. If the specific key has been initialized earlier in a different part of the application the provided init value will be ignored as it is for the further calls in the same component after each re-rendering. (It is how standard useState behaves.)

useInterstate({key1: initValue1, key2: initValue2})

The second call interface is similar to what is using in our other hook useMultiState. And as in the case of useMultiState, this call interface replaces multiple uses of the hook in one component.

function Component1() {
  const [, setState] = useInterstate({
    color: 'blue',
    theme: { textSize: 'medium', hyphenations: true },
    density: 100,
  });

  return <ControlComponent {...setState} />;
}

function Component2() {
  const [useSubscribe] = useInterstate({
    color: 'blue',
    theme: { textSize: 'medium', hyphenations: true },
    density: 100,
  });
  const state = useSubscribe();

  return <DisplayComponent {...state} />;
}

Notable differences with useMultiState:

  • The properties' names are global and corresponding to the global state keys.

  • useInterstate does not return the object with the values of the state in its relevant properties. Instead, it returns a hook whose call provides such an object. It means you need one more step to subscribe as it was described for the first call interface.

    Effectively, instead of the construction:

    const [useSubscribeToKey1, setStateForKey1] = useInterstate(key1, initValue1);
    const state1 = useSubscribeToKey1();
    
    const [useSubscribeToKey2, setStateForKey2] = useInterstate(key2, initValue1);
    const state2 = useSubscribeToKey1();

    you can write:

    const [useSubscribe, { key1: setStateForKey1, key2: setStateForKey2 }] = useInterstate({
      key1: initValue1,
      key2: initValue2,
    });
    const { key1: state1, key2: state2 } = useSubscribe();

If you need to have setters for one set of keys and subscribe to another one, it is better to use the hook twice with different init objects aiming performance optimization.

const [, state] = useInterstate({
  key1: initValue1,
  key2: initValue2,
});
const [useSubscribe] = useInterstate({
  key2: initValue2,
  key3: initValue3,
  key4: initValue4,
});
const state = useSubscribe();

Enhanced interface

We add an enhanced interface allowing us to obtain a setter along with the value of the state in the easiest way. It adds methods get, set, and both to the returned value. Let us take a look at different scenarios.

If we only need to have a function to update the sate:

const [, setState] = useInterstate('size', 9);

It is equivalent to the following:

const setState = useInterstate('size', 9).set();

Now we want to subscribe to changes in the state (that will cause re-rendering the component):

const [useSubscribe] = useInterstate('size', 9);
const state = useSubscribe();

That is the same as:

const state = useInterstate('size', 9).get();

Both:

const [useSubscribe, setState] = useInterstate('size', 9);
const state = useSubscribe();

Now write it in a compacter way:

const [state, setState] = useInterstate('size', 9).both();

It also works with the multi-state call interface.

Important notes

There are some limitations to using useInterstate that, when being broken, may lead to errors. Also, some advanced use cases may add power and resilience to your code.

  • One of the powerful abilities of useInterstate is that it can resubscribe to different keys of the global state dynamically.

    const [useSubscribeReceivedKey] = useInterstate({ passKey: 'city', initVForReceivedKey: 'NY' });
    const received = useSubscribeReceivedKey();
    // Subscribe to a pair key and default value for the second useInterstate. If they change the
    // second hook will dynamically resubscribe to a new key.
    
    const [useSubscribe, setState] = useInterstate(received.passKey, received.initVForReceivedKey);

    Every time a key value that is passed to useInterstate changes the hook goes back to the stage where it sees whether the record for the new key has not been initialized, so useInterstate tries to initialize the record using a provided argument. After that, you become subscribed to the new key of the state and can manipulate it.

    It is true for the first call interface. When you pass a multi-state object for the first time the hook will memoize it and use it throughout the life of the component regardless you change it later. If you switch from one interface to another it will cause an error.

  • Before accessing the value of a particular key of the state it must be initialized with a provided default value.

    const [useSubscribe, setState] = useInterstate('name');
    // A default value is not provided in the hook and other parts of the code for this key
    
    const state = useSubscribe();
    // Accessing to the state, but it was not initialized, so an error occurs
    setState('Vincent van Gogh');
    // Calling setState is reckoned as access to the state, so still error
  • It is not possible to initialize the key by omitting the default value or explicitly passing undefined. If you need to have the default value being undefined, then passing a function returning undefined as an init argument is required. If you omit any default value or pass undefined, it will mean skipping the initializing step.

    const [useSubscribe, setState] = useInterstate('color', () => undefined);
    // Initializing the state of a key with undefined value
    
    const [useSubscribe, setState] = useInterstate('color');
    // Skip initializing
  • If you need to initialize the key with a function value, you pass a function returning the desired function.

    const [useSubscribe, setState] = useInterstate('getLink', () => () => {
      // ...
    });
    // Initializing with a function value
  • You might run into trouble if you try to initialize a key giving differing default values in various parts of the application concurrently.

    useInterstate('pitch', 'C');
    
    // ...
    
    useInterstate('pitch', 'D');
    // If you initialize concurrently in another part of the code with a different default value,
    // it causes an error
    useInterstate('pitch', 'A');
    
    // ...
    
    useInterstate('pitch', 'A');
    // It's Ok
  • If a state for a key has already been initialized, a default value passed to useInterstate will be ignored.

  • If you try to change the value of the same key in different parts of the application at the same moment it will lead to an error.

    setState(1);
    setState(2);
    // If they are called both, it will throw an error
  • A setter function returned by useInterstate will not change during the component life and always has a stable identity similar to the standard hook useState.

Scope

The notable distinction of useInterstate comparing with other global state management solutions is it does not require wrapping the whole tree into a specific context provider component. The library is working with no additional requirements. It is especially important when you prototyping as you do not need to get interrupted by annoying duties to make specific time-consuming prerequisites. Unique keys are in use broadly within the boundaries of the entire application with the same values for any given key right after the first component with useInterstate taking this key as an argument has been rendered. But what if we want to use the identical keys in different parts of the code in isolation? The library provides a special wrapping component Scope. It cuts a branch of components tree giving it a separate space where you have an isolated state. It is useful when you need to make a reusable component driven by useInterstate state management. You have to wrap the component with the Scope tag, otherwise, multiple instances of this component in the common components tree will interfere with each other sharing the same state.

import { Scope } from '@smart-hooks/use-interstate';

const Component = () => (
  <Scope>
    <ComponentWitUseInterstate />
  </Scope>
);

Error handling

If you plan using Error Boundaries (and you should), you have two functions handy. To determine whether an error occurs from the misuse useInterstate, there is a function isUseInterstateError.

import { isUseInterstateError } from '@smart-hooks/use-interstate';

// ...

if (isUseInterstateError(error)) {
  // ...
})

To clean up after an error occurred you call getUseInterstateErrorsHandleMethods receiving a method flushValueOfKey. It removes inconsistency in the records linked to a key where the error occurred. The method accepts a boolean flag that orders to revert a value of the key to an old state if it is true.

import { getUseInterstateErrorsHandleMethods } from '@smart-hooks/use-interstate';

// ...

const methods = getUseInterstateErrorsHandleMethods(error);

if (methods && methods.flushValueOfKey) {
  methods.flushValueOfKey(true);
}

Security remarks

When you use third-party components there is a risk that they may have unrestricted access to the state of the entire application being able to use the same keys names. The state management provided by useInterstate could be isolated from ancestors in the components tree structure by Scope. But it is annoying to wrap every imported component in Scope to prevent its access to the app state.

There are two approaches to avoid this scenario:

  1. You can use unique Symbols as keys in your state structure.

  2. There is an alternative way to import useInterstate that will guarantee a separate space for the entire application restricting access from outer components.

    // useInterstate.js
    import { getUseInterstate } from '@smart-hooks/use-interstate';
    
    const { Scope, useInterstate } = getUseInterstate();
    
    export { Scope, useInterstate };
    import { Scope, useInterstate } from './useInterstate.js';

    Just remember to use the same instances of the hook and Scope component across the entire app.

Typescript and managing complicated state structures

When using useInterstate as a JavaScript function it feels much handier in situations where a developer wants a rapid simple solution. Drafting and prototyping are such examples. But when it comes to a need for managing a complicated various state structure useInterstate becomes weak and not so useful. JavaScript developer must trace every key name and its relation to the meaning of the particular state record on their own. It is a significant limitation of using this hook in big projects.

The good news is we have cared about the capabilities of useInterstate to put its powerful potential and resilience into projects of any grade of sophistication. And it is with no sacrifice in terms of simplicity of use. Everything you need is to provide a state interface to getUseInterstate within the framework of Typescript. On its return, you will have a useInterstate hook that remembers the interface so you will never get lost.

interface State {
  activeUser: UserRecord;
  permissions: string[];
  premiumStatus: boolean;
  cart: CartState;
}

const { useInterstate } = getUseInterstate<State>();

const [useSubscribe01] = useInterstate('activeUser', undefined);
const activeUser01 = useSubscribe01(); // UserRecord

declare const rememberActiveUser: UserRecord;
const [useSubscribe02] = useInterstate('activeUser', rememberActiveUser);
const activeUser02 = useSubscribe02(); // UserRecord

const [useSubscribe03] = useInterstate<'permissions' | 'premiumStatus'>('permissions', undefined);
const activeUser03 = useSubscribe03(); // boolean | string[]

const [useSubscribe04] = useInterstate({ activeUser: undefined, cart: undefined });
const activeUser04 = useSubscribe04();
// { readonly activeUser: UserRecord; readonly cart: CartState }

const [useSubscribe05] = useInterstate('activeUser', 'John Doe');
// Error: "activeUser" property is not a string

const [useSubscribe06] = useInterstate({ activeUser: 'John Doe', cart: undefined });
// Error: "activeUser" property is not a string

const [useSubscribe07] = useInterstate({ premiumStatus: true, subscriptionDate: undefined });
// Error: "subscriptionDate" property is missing in State interface

const [useSubscribe08] = useInterstate('gender', 'female');
// Error: "gender" property is missing in State interface

It is also fine to use getUseInterstate without providing a state interface. In this case, it is the responsibility of a developer to maintain the soundness of data types.

const { useInterstate } = getUseInterstate();

const [useSubscribe01] = useInterstate('activeUser', undefined);
const activeUser01 = useSubscribe01(); // unknown

declare const rememberActiveUser: UserRecord;
const [useSubscribe02] = useInterstate('activeUser', rememberActiveUser);
const activeUser02 = useSubscribe02(); // UserRecord

const [useSubscribe03] = useInterstate<UserRecord>('activeUser', 'John Doe'); // Error

const [useSubscribe04] = useInterstate<string[]>('permissions', undefined);
const activeUser04 = useSubscribe04(); // string[]

const [useSubscribe05] = useInterstate<{ activeUser: UserRecord; cart: CartState }>({
  activeUser: undefined,
  cart: undefined,
});
const activeUser05 = useSubscribe05();
// { readonly activeUser: UserRecord; readonly cart: CartState }

const [useSubscribe06] = useInterstate<{ activeUser: UserRecord; cart: CartState }>({
  activeUser: 'John Doe',
  cart: undefined,
});
// Error: activeUser is not a string
5.1.2

3 years ago

5.1.1

3 years ago

5.1.0

4 years ago

5.0.0

4 years ago

4.1.1

4 years ago

4.1.0

4 years ago

4.0.0

4 years ago

3.1.1

4 years ago

3.1.0

4 years ago

3.0.0

4 years ago

2.1.4

4 years ago

2.1.3

4 years ago

2.1.2

4 years ago

2.1.1

4 years ago

2.1.0

4 years ago

2.0.0

4 years ago