DEV Community

loading...
IT Minds

A game of states

Simon Bundgaard-Egeberg
I have a passion for the bleeding edge, and I love watching a great community evolve and help each other
Originally published at insights.it-minds.dk ・4 min read
Twas a cold winter morning, among the fields the actions were slowly waking up. 
"Sire, it is never fast to startup". The sire knew, however, he did not know of any alternatives. 
"I just wish it was easy," said peasy, the sire and dispatched a thunk to fetch some milk.

Easy-Peasy state management in React

There are many state management solutions out there for React. Some of them good, and some of them great.
In this article, I will tell you a little bit about easy-peasy. Why I think this state management library is amazing.

I remember looking at svelte and thinking, damn, I wish global state in react could be one line as well.
But alas, twas not.
I write all my frontend code in TypeScript. It has its strengths definitely, but it does indeed have weaknesses as well, here I am mostly talking about writing extra code. Redux is in and of itself a decent state management library. The tooling around redux, however, is legendary. (Here I am mainly talking about the redux dev-tools).
But the sheer lines of boilerplate code (with TypeScript) that goes into a redux action is insane.

How about just writing 9 lines for a new state branch in a single file?

const store = createStore({
  subjects: {
    loyalSubjects: [],
    addLoyalSubject: action((state, payload) => {
      subjects.loyalSubjects.push(payload)
    })
  }
});

How about a declarative declaration of the stateroot with no hassel?

function App() {
  return (
    <StoreProvider store={store}>
      <Kingdom />
    </StoreProvider>
  );
}

What about just using hooks to get the state you want?

function Kingdom() {
  const loyalSubjects = useStoreState(state => state.subjects.loyalSubjects)
  const addLoyalSubjects = useStoreActions(actions => actions.subjects.addLoyalSubjects)
  return (
    <Farms>
      {loyalSubjects.map((loyalSubject, idx) => <div key={idx}>{loyalSubject}</div>)}
      <HireLoyalSubject onAdd={addLoyalSubjects} />
    </Farms>
  )
}

Very good, but I can already do that with ...

Let's get one thing clear, we can do everything with anything. However, our perception of the thing differs from tool to tool.

Let's talk about attacks on the kingdom?
An asynchronous action in Easy-Peasy is called a thunk. In the Kingdom, a raid on other countries would be a thunk.
You will send your soldiers to attack, and they would return at some point, and when they do, we need to pay them dollar bills.

Let's build upon the store.

const warefareModule = {
  kingdom: [],
  army: [],
  attackCountry: thunk(async (actions, payload) => {
    // do warefare stuff, and win a country
    actions.addCountryToKingdom(country)
  }),
  addCountryToKingdom: action((state, payload)=> {
    state.kingdom.push(payload);
  }),
  paySoldiers: actionOn(
    (actions) => [actions.addCountryToKingdom],
    (actions, payload, {getState})     => {
      const soldiers = getState().army;
      soldiers.forEarch(soldier=>soldier.pay(10))
  })
}

The kingdom would be an array of countries, and the army an array of soldiers.
The thing to notice here is that the attackCountry is an asynchronous action (thunk), and when that has run, it will add the country (if you won the war) to the array of countries in the kingdom. When this action has run, we have added a listener (paySoldiers), which will then run its code.

However, the types from above are not immediately apparent and in my own opinion using typescript makes code more self-documenting.

Typing easy-peasy is not hard if you take the time to read their API.

Let's type the warfare module

type Soldier = {
    // soldier type
}

type Country = {
    // country type
}

type WarfareModule = {
    kingdom: Country[],
    army: Soldier[],
    // the thunk is a type from easy peasy
    attackCountry: Thunk<WarfareModule>,
    // the second type here is the payload
    addCountryToKingdom: Action<WarfareModule, Country>
    paySoldiers: ActionOn<WarfareModule>
}

const warefareModule: WarfareModule = {
    kingdom: [],
    army: [],
    attackCountry: thunk(async (actions, payload) => {
        // do warefare stuff
    }),
    addCountryToKingdom: action((state, payload)=> {
        state.kingdom.push(payload);
    }),
    paySoldiers: actionOn(actions => [actions.addCountryToKingdom], (state, payload) => {
        state.soldiers.forEarch(soldier=>soldier.pay(10))
    })
}

not too shabby. More on the typescript API https://easy-peasy.now.sh/docs/typescript-api/.

Last, but not least, Easy-Peasy has an injection system which enables you to inject different things into your thunks. Perfect for dependency injecting a network API, for even creating a repository pattern for your network layer, and disconnect your code from networking.

Networking at parties like it's 1399

War changes. And therefore Peasy, the sire must be prepared with a functional store model, open for changes in.

interface INetworking {
    startAWar: ()=>Promise<Country>
}

type Injections = {
    network: INetworking
}

class NetworkImpl implements INetworking {
    private apiClient;

    constructor(){
      this.apiClient = new APIClient();
    }

    async startAWar() {
        let country: Country = {};
        country = await apiClient.get("somePath")
        return new Promise<Country>((()=>country))
    }
}

const injections = {
    network: new NetworkImpl()
}

const store = createStore({
  subjects: {
    loyalSubjects: [],
    addLoyalSubject: action((state, payload) => {
      subjects.loyalSubjects.push(payload)
    })
  }
}, {injections});

And just like that, we have created a little repository pattern like injection into our store. And as long as we adhere to our interface, the Kingdom will be flexible in its networking.

Let's remake our warfare module to use the injected API.

type WarfareModule = {
    kingdom: Country[],
    army: Soldier[],
    // Injections is the type we created
    attackCountry: Thunk<WarfareModule, unknown, Injections>,
    // the second type here is the payload
    addCountryToKingdom: Action<WarfareModule, Country>
    paySoldiers: ActionOn<WarfareModule>
}

const warefareModule: WarfareModule = {
    kingdom: [],
    army: [],
    attackCountry: thunk(async (actions, payload, {injections}) => {
        const country = await injections.networking.startAWar();
        actions.addCountryToKingdom(country);
    }),
    addCountryToKingdom: action((state, payload)=> {
        state.kingdom.push(payload);
    }),
    paySoldiers: actionOn(actions => [actions.addCountryToKingdom], (state, payload) => {
        state.soldiers.forEarch(soldier=>soldier.pay(10))
    })
}

Talk about full circle!

Easy Peasy https://easy-peasy.now.sh/
- Typescript tutorial https://easy-peasy.now.sh/docs/typescript-tutorial/
- Typescript API https://easy-peasy.now.sh/docs/typescript-api/

Discussion (0)

Forem Open with the Forem app