author: Krasimir Tsonev

Krasimir is a blogger, who writes and speaks.
He loves open source and codes awesome stuff.

Jolly Roger - a 2KB micro-framework based on React hooks API

The hooks API is a wonderful idea. There are some slick patterns involved that push the React development to a more functional approach. I'm interested in trying that new API and decided to use it for my latest project. However, after a couple of days, it looked like I can't build my app only with hooks. I needed something else. And that's mainly because each hook works on a local component level. I can't really transfer state or exchange reducers between the components. That's why I created Jolly Roger. It has similar helpers but works on a global app level.

Sharing state

Let's have a look at the following example:

import react, { useEffect, useState } from 'react';

const App = function () {
  const [ time, setTime ] = useState(new Date());

  useEffect(() => {
    setInterval(() => setTime(new Date()), 1000);
  }, [])

  return (
    ...
  );
}

It's a component that has a local state called time. Once we mount it we trigger an interval callback and change the state every second. Now imagine that we want to use the value of time in other components - Clock and a Watch. There are somewhere deeper in our React tree and we can't just pass the time around:

function Clock() {
  const [ time ] = useState(<?>);

  return <p>Clock: { time.toTimeString() }</p>;
}

function Watch() {
  const [ time ] = useState(<?>);

  return <p>Watch: { time.toTimeString() }</p>;
}

That's not really possible because the idea of the useState hook is to create a local component state. So time is available only for the App but not Clock and Watch. This is the first problem that Jolly Roger solves. It gives you a mechanism to share state between components.

function Clock() {
  const [ time ] = roger.useState('time');

  return <p>Clock: { time.toTimeString() }</p>;
}

function Watch() {
  const [ time ] = roger.useState('time');

  return <p>Watch: { time.toTimeString() }</p>;
}

const App = function () {
  const [ time, setTime ] = roger.useState('time', new Date());

  useEffect(() => {
    setInterval(() => setTime(new Date()), 1000);
  }, [])

  return (
    <Fragment>
      <Clock />
      <Watch />
    </Fragment>
  );
}

Here is a online demo.

The API is almost the same except that we have to give the state a name. In this case that is the first argument of the Roger's useState equal to time. We have to give it a name because we need an identifier to access it from within the other components.

Using a reducer

Now when we have a global state we may define a reducer for it. You know that nice function that accepts your current state and an action and returns the new version of the state in immutable fashion. Let's build on top of the previous example and say that we will set the time manually. We are going to dispatch an action giving the new time value and the reducer has to update the state.

roger.useReducer('time', {
  yohoho(currentTime, payload) {
    console.log('Last update at ' + currentTime);
    console.log('New ' + payload.now);
    return payload.now;
  }
});

Again the important bit here is the name time because that's how Roger knows which slice of the application state to manipulate. In here we are creating an action with a name yohoho that receives the current time and some payload. In this example we don't want to do anything else but simply set a new time so we just return what's in the now field. We will also create a new component that will trigger this action:

function SetNewTime() {
  const { yohoho } = roger.useContext();

  return (
    <button onClick={ () => yohoho({ now: new Date() }) }>
      click me
    </button>
  );
}

Here is a online demo to play with.

What is useContext we'll see in the next section.

Using the context

Because Roger works as a global singleton it can store some stuff for you. By default the actions that we define in our useReducer calls are automatically stored there. As we saw in the previous section the yohoho method is used by getting it from the Roger's context. We can do that with the help of the useContext method. Let's continue with our little time app and get the time from an external API via a fetch call. In this case we may create a function in the Roger's context that does the async request for us and fires the yohoho action.

roger.context({
  async getTime(url, { yohoho }) {
       const result = await fetch(url);
    const data = await result.json();

    yohoho({ now: new Date(data.now)})
  }
});

Every context method accepts two arguments. The first one is reserved for anything that needs to be injected as a dependency and the second one is always the Roger's context itself. In our case getTime has one dependency and that's the endpoint URL. We need the yohoho action which is defined in our useReducer call. We get the data and dispatch the action. This updates the application state and makes the component re-render.

Here is the same <SetNewTime> component using the new getTime helper:

function SetNewTime() {
  const [ inProgress, setProgress ] = useState(false);
  const { getTime } = roger.useContext();

  const onClick = async () => {
    setProgress(true);
    await getTime('https://igit.dev/now');
    setProgress(false);
  }

  return (
    <button onClick={ onClick } disabled={ inProgress }>
      { inProgress ? 'getting the time' : 'click me' }
       </button>
  );
}

Notice that Jolly Roger plays absolutely fine with the native React hooks. Like we did here we are setting an inProgress flag to indicate that there is a request in progress. Check out how it works here.

You like it?

If you like what you are seeing you may be interested in checking the repository of the library. There is the full API described. Also here is the Jolly Roger visualized.


blog comments powered by Disqus