React with hooks and Typescript

React with hooks and Typescript

<strong>This post expects you to have had minor exposure to Typescript. If this isn’t the case I would recommend that you firstly read an introductory post, such as [this one](</strong><a href="https://medium.freecodecamp.org/typescript-javascript-with-super-powers-a333b0fcabc9)." target="_blank"><strong>https://medium.freecodecamp.org/typescript-javascript-with-super-powers-a333b0fcabc9).</strong></a><strong> Not required, but certainly will help.</strong>

This post expects you to have had minor exposure to Typescript. If this isn’t the case I would recommend that you firstly read an introductory post, such as [this one](https://medium.freecodecamp.org/typescript-javascript-with-super-powers-a333b0fcabc9).. "https://medium.freecodecamp.org/typescript-javascript-with-super-powers-a333b0fcabc9).")** Not required, but certainly will help.**

React hooks are officially a thing. Woot! I previously wrote about Easy Peasy, a global state library for React that leverages hooks and focuses on having a super simple but powerful API.

easy-peasy.js

import { StoreProvider, createStore, useStore, useActions } from 'easy-peasy';

// 👇 create your store, providing the model
const store = createStore({
  todos: {
    items: ['Install easy-peasy', 'Build app', 'Profit'],
    // 👇 define actions directly on your model
    add: (state, payload) => {
      // do simple mutation to update state, and we make it an immutable update
      state.items.push(payload)
      // (you can also return a new immutable instance if you prefer)
    }
  }
});

const App = () => (
  // 👇 wrap your app to expose the store
  <StoreProvider store={store}>
    <TodoList />
  </StoreProvider>
)

function TodoList() {
  // 👇  use hooks to get state or actions
  const todos = useStore(state => state.todos.items)
  const add = useActions(actions => actions.todos.add)
  return (
    <div>
      {todos.map((todo, idx) => <div key={idx}>{todo}</div>)}
      <AddTodo onAdd={add} />
    </div>
  )
}

This post is going to take things to a whole new level by showing you how you can compliment this API with the power and safety offered by Typescript.

I am going to take us step by step through building a naive todos application. Whilst this is completely unimaginative, I hope its boring familiarity will allow readers to more easily focus on the typing and APIs.

The code for this post exists in this GitHub repository.

Right, let’s get to it.

Set up

To get going as fast as possible we’ll leverage Create React App, along with its Typescript support.

Firstly, bootstrap an application, specifying that it support Typescript.

npx create-react-app type-safe-state-ftw --typescript

Ok, change directory into your newly created application.

cd type-safe-state-ftw

Then install Easy Peasy.

npm install easy-peasy

This single dependency includes everything you to support robust global state within your application. Some of its features includes:

  • Derived state
  • Remote data fetching/persisting
  • Auto memoization for performance
  • Simple mutation based API that converts to immutable updates
  • React Hooks to use with our components
  • Full Redux interoperability (including Redux Dev Tools)

And it only carries a very respectable 9kb gzip cost — all dependencies included.

Defining our Model

We will start by creating types to represent the model for our store.

model.ts

type TodosModel = {
  items: string[];
}

type NotificationModel = {
  msg: string;
}

export type StoreModel = {
  todos: TodosModel;
  notification: NotificationModel;
}

Fairly self explanatory. We will have some todos along with a notification msg.

Create our Store

Now we can use the StoreModel whilst creating our store.

store.ts

import { createStore } from 'easy-peasy';
import { StoreModel } from './model';

//                           👇
const store = createStore<StoreModel>({
  todos: {
    items: ["Install easy-peasy", "Build app", "profit"]
  },
  notification: {
    msg: ""
  }
});

export default store;

By providing our StoreModel as a type parameter on createStore we get two things: type checking that ensures our implementation matches the StoreModel type, and auto completion whilst we do so.

The returned store now has all the type information baked in, so any interaction with it will have Typescript helping us.

FYI — createStore returns a Redux Store, with all the Store APIs being available against it.### Grabbing state directly off the Store

Sweet, let’s try grab some state directly of the store.

getState.ts

store.getState().todos.items;
// ["Install easy-peasy", "Build app", "profit"]

This operation is fully typed.

Zing.

Defining actions to mutate state

Actions are responsible for updating the state on our model. Let’s expand our model types to indicate where we intend the actions to exist.

actions.ts

import { Action } from "easy-peasy";

type TodosModel = {
  items: string[];
  add: Action<TodosModel, string>; // 👈
}

type NotificationModel = {
  msg: string;
  set: Action<NotificationModel, string>; // 👈
}

export type StoreModel = {
  todos: TodosModel;
  notification: NotificationModel;
}

The Action type is a generic type, where its first type argument is the model that it is operating against, and the second type argument is the type of the payload it will receive.

action-def.ts

//         👇model to operate against
Action<TodosModel, string>;
//                   👆payload

It’s also completely fine for an Action to contain no payload.

Implementing actions to update state

After updating our StoreModel Typescript will be shouting at us that our store implementation does not meet its expectations.

Let’s implement the missing actions against our store.

actions-imp.ts

const store = createStore<StoreModel>({
  todos: {
    items: ["Install easy-peasy", "Build app", "profit"],
    // 👇
    add: (state, payload) => {
      state.items.push(payload);
    }
  },
  notification: {
    msg: "",
    // 👇
    set: (state, payload) => {
      state.msg = payload;
    }
  }
});

You can see the actions receive the state they relate to as well as any payload that was provided to them. We use the actions to update our state — under the hood these mutations are converted into an immutable update against our store.

FYI you can also return “new immutable state” within your actions if you prefer. I prefer the “mutable” form as it’s less error prone and more concise, but this is a matter of personal preference.

alt-action-form.ts

const store = createStore<StoreModel>({
  todos: {
    // ...
    add: (state, payload) => {
      return {
        ...state,
        items: [
          ...state.items,
          payload
        ]
      };
    }
  },
  // ...
});

We will get type checking and autocomplete whilst implementing the actions.

Firing actions directly via the store

Similar to accessing state directly against the store, we can also fire our actions via the store. They are bound to the store’s dispatchat the same path as they were defined within our model.

dispatch.ts

store.dispatch.todos.add("Finish reading this article");

And yep, typed again.

Expose the store to your application

Before we can use our store within our React components we need to wrap our application with the StoreProvider, providing the store instance.

app.tsx

import React from "react";
import { render } from "react-dom";
import { StoreProvider } from "easy-peasy";  // 👈
import store from "./store";

render(
  <StoreProvider store={store}>
    <App />  
  </StoreProvider>,
  document.querySelector("#root");
);

Almost there.

Getting our hooks ready

Easy Peasy ships with _hook_s to interact with the store within our React components. However, it will be great if we can ensure that the hooks contain all the type information about our store so that we don’t lose any of the type safety and autocomplete goodness we have gotten used to.

We can create pre-baked typed versions of our hooks via the [createTypedHooks]([https://github.com/ctrlplusb/easy-peasy#createtypedhooks)](https://github.com/ctrlplusb/easy-peasy#createtypedhooks) "https://github.com/ctrlplusb/easy-peasy#createtypedhooks)") helper.

hooks.ts

import { createTypedHooks } from "easy-peasy";
import { StoreModel } from "./model";

const { 
  useActions, 
  useStore, 
  useDispatch 
} = createTypedHooks<StoreModel>();

export { useActions, useDispatch, useStore };

Now whenever we need to use a hook from our hooks.ts file they will be able to guard and guide us via the type information we provided to them.

Hooking it all up

The moment we have been leading up to. Here’s how we use our state and actions within our components.

todos.tsx

import { useState } from "react";
import { useStore, useActions } from "./hooks"; // 👈

export default function Todos() {
  // 👇 pull out state from our store
  const items = useStore(state => state.todos.items);

  // 👇 pull out actions from our store
  const add = useActions(actions => actions.todos.add);

  const [newTodo, setNewTodo] = useState("");

  return (
    <div>
      <ul>
        {items.map((todo, idx) => 
           <li key={idx}>{todo}</li>
         )}
      </ul>
      <input 
        type="text"
        onChange={e => setNewTodo(e.target.value)}
        value={newTodo}
      />
      <button onClick={() => add(newTodo)}>Add</button>
    </div>
  );
}

This Todos component pulls out the todo items from the store, and subsequently renders them in a list. It allows us to add a new todo item. You will note that we not keeping our form state within our store. I highly recommend that transient state, such as form data, be created and used within the scope of your components themselves. In our example above we use the standard useState hook that React provides to help us with this.

This implementation is very naive, and could definitely do with some more work, but I’ve intentional kept it simple to make it quicker to read and understand what is going on.

As we consuming our typed hooks within our component we get all the type benefits.

This can be hugely helpful when refactoring your store as you will immediately get error messages in any component that is consuming refactored state — allowing you to quickly cycle through the instances and fix them.

What about side effects?

We can encapsulate operations such as network requests within “thunks”.

Let’s adjust the requirements for our store. We now need to ensure that any new todo items are sent to the backend via network request before adding it to the store.

We can use theThunk to describe an action that will allow us to encapsulate this behaviour.

thunk.ts

import { Action, Thunk } from 'easy-peasy';

type TodosModel = {
  items: string[];
  saved: Action<TodosModel, string>; 
  save: Thunk<TodosModel, string>; // 👈
}

We have done two things here. Firstly, we added the save thunk action, represented by the Thunk type. We then renamed the existing add action to saved. The thought process being is that we will first persist our new todo via the save thunk action, and subsequently add it to the store via the saved action. It will become clearer later on why we have a pair of actions for this.

Let’s update our store implementation accordingly.

thunk-model.ts

import { thunk } from "easy-peasy"; // 👈

const store = createStore<StoreModel>({
  todos: {
    items: ["Install easy-peasy", "Build app", "profit"],
    // 👇renamed
    saved: (state, payload) => {
      state.items.push(payload);
    },
    // 👇our thunk
    save: thunk(async (actions, payload) => {
      await todoService.save(payload);
      actions.saved(payload);
    })
  },
  // ...
});

As you can see the “thunk” action doesn’t receive the model state. Instead it receives the actions of the model it relates to. Therefore if we wish to update state we need to dispatch other “standard” actions to do so. This model is very much the same as found in popular libraries such as redux-thunk.

Within our save thunk action we make a call to our todoService, then we dispatch the saved action to ensure the todo is added to our store.

We can now update our component, ensuring that it calls the newly created save thunk action instead.

todos-thunk.tsx

export default function Todos() {
  const items = useStore(state => state.todos.items);

  //    👇 
  const save = useActions(actions => actions.todos.save);

  const [newTodo, setNewTodo] = useState("");

  return (
    <div>
      ...
      <button onClick={() => save(newTodo)}>Add</button>
    </div>
  );
}

What else?

There are many other features — which I will avoid going through right now out of fear of creating an overly long post. Some of the additional elements you can declare on your model include:

  • Derived state
  • Remote data fetching/persisting
  • Auto memoization for performance
  • Simple mutation based API that converts to immutable updates
  • React Hooks to use with our components
  • Full Redux interoperability (including Redux Dev Tools)

I encourage you to read the Easy Peasy GitHub page for more information about the library itself, view a more extensive example of the Typescript integration, and get detailed information in regards to the APIs.

In case you missed it, the code for this post lives in this GitHub repository.

Thanks for your time

I hope you enjoyed the read. Any and all feedback is greatly welcomed.

Feel free to log any issues via the GitHub repo.

Or take the following course:

Master ReactJS: Learn React JS from Scratch

Learn ReactJS: Code Like A Facebook Developer

ReactJS Course: Learn JavaScript Library Used by Facebook&IG

React: Learn ReactJS Fundamentals for Front-End Developers

React From The Ground Up

Angular 9 Tutorial: Learn to Build a CRUD Angular App Quickly

What's new in Bootstrap 5 and when Bootstrap 5 release date?

Brave, Chrome, Firefox, Opera or Edge: Which is Better and Faster?

How to Build Progressive Web Apps (PWA) using Angular 9

What is new features in Javascript ES2020 ECMAScript 2020

Why ReactJS is better for Web Application Development?

Web Application Development is the point of contact for a business in today's digital era. It is important to choose the right platform for Web Application Development to build a high end Web

ReactJS Development Services | ReactJS Development Company - Chapter 247 Infotech

Chapter 247 Infotech is a leading ReactJS development company in India, USA, offering ReactJS development services at par to a spectrum of business domains from E-commerce, healthcare to Edutech at

Pagination in ReactJs

There are a lot of resourceful materials online that give good insights into pagination in ReactJs, as well as NPM packages you can easily use