Saturday, August 20, 2022

Using Recoil with SPFx

Try out the latest alternative to Redux with SPFx


When developing with Redux or MobX, we have had to deal with the inherent complexity of these technologies. We all wish that there would be something that is simpler and easier to implement, some tech that feels more native to React programming with hook like syntax.

Is there a new technology that we can use? Yes, it's from Facebook and is called RecoilJS or simply Recoil

What is Recoil

Recoil is a State management library that allows you to define global state in manageable chunks called Atoms (instead of one large state object in Redux) and use them in your React components across your application, either directly or via selectors. Recoil Selectors are pure functions that are re-evaluated when the up-stream atoms are updated.

You can find more about Recoil at the official Website - https://www.recoiljs.org

Advantages of Recoil

  • Simple and easier syntax compared to Redux
  • Use of modern React Hooks
  • Multiple state objects (atoms) instead of one monolithic global state object
  • Pure function based selectors instead of switch-case based reducers
  • selectors are re-evaluated when atoms are updated
  • Selectors can derive from other selectors
  • Support for synchronous and asynchronous selectors

Core Concepts of Recoil

Recoil has two core concepts


  • Atoms - Atoms are global shared state objects. Each Atom has a unique key meaning that no two atoms can have the same key. Atoms are used as global state objects instead of lifting the React component state up.Atoms can be updated and are subscribable. Any component that uses an Atom value is refreshed atomatically whenever the value of that atom changes.

Lets say we want to store the information about a customer in a global state object. This can be done by defining the same as an atom -

  • To define an atom, create a separate file, say GlobalState.ts.
  • Define the interface for the Customer.
  • Next write the imports and define the atom as shown below -

GlobalState.ts

// GlobalState.ts - Global atoms and selectors
import { atom, RecoilState } from 'recoil';

export interface ICustomer {
    id: number;
    name: string;
    limit: number;
}

export const custState: RecoilState<ICustomer> = atom({
    key: 'custatom1',
    default: {
        id: 0,
        name: '',
        limit: 0
    }
});

The above code defines an atom (global state object) that we can use in the entire component hierarchy of the React App or SPFx React component tree.

  • Selectors - Selectors are pure functions that accept atoms or other selectors as input. Whenever the upstream atoms or selectors are updated, these selectors are re-evaluated. Components subscribing to the selectors are re-rendered whenever the atoms or selectors that they depend on, are changed.

Selectors can be defined in the same file where the atoms are defined. In our case we will write the code for the selector in GlobalState.ts.

First update the imports section to import a selector in addition to atom and RecoilState as shown below -

    import { atom, RecoilState, selector } from 'recoil';

Next let's write a selector function to return the Customer information as a string -

    export const customerInfo = selector({
        id: 'customerInfo',
        get: ({get}) => {
            const cust = useRecoilState(custState);
            return `${cust.id} - ${ cust.name } - ${cust.limit}`;
        }
    });

In the above code, the useRecoilState hook allows us to fetch the Atom as a plain object which is stored in the variable called cust. The selector 'customerInfo' then returns an interpolated string with the customer data.

Whenever the custState changes, customerInfo will be re-evaluated and all the components that are using the customerInfo selector will be refreshed.

How to consume the recoil atoms/selectors in components

There are two ways to consume the Global state. The first one is by consuming an atom value and the second is by consuming the value returned by a selector. To demonstrate this, let's create two components, the first one will use an atom value directly and the second one will use a selector.

CustomerData.ts

This component imports the custState atom from GlobalState.ts and makes use of the useRecoilValue hook provided by recoil to consume the custState object values.

// CustomerData.ts - Display customer data dirctly from custState atom
import * as React from 'react';
import { useRecoilValue } from 'recoil';

import { custState } from './GlobalState';

export default function CustomerInfo() : JSX.Element {

    const cust = useRecoilValue<ICustomer>(custState);

    return (
        <div>
            <h1>Customer Details -</h1>
            <div>
                ID: { cust.ID } <br/>
                Name: { cust.Name } <br/>
                Limit: { cust.Limit }
            <div>
        </div>
    );
}

Whenever any component updates the custState atom, CustomerData component will be refresh automatically.

CustomerDetails.ts

This component uses a Recoil selector instead of a Recoil Atom. Remaining logic remains the same. Since the selector customerInfo returns a string, the useRecoilValue hook uses string as the type

// CustomerDetails.ts - Display customer details via the CustomerInfo selector
import * as React from 'react';
import { useRecoilValue } from 'recoil';

import { customerInfo } from './GlobalState';   // selector

export default function CustomerDetails() : JSX.Element {

    const custInfo = useRecoilValue<string>(customerInfo);  

    return (
        <div>
            <h1>Customer Details -</h1>
            <div>
                { custInfo }
            <div>
        </div>
    );
}

Please note how, in both components, useRecoilValue hook is used.

Setting up the components

In order to use these components, a root component, preferably the App component or a child-component of the App component must include a special RecoilRoot component. Recoil-based components will only work when placed under the RecoilRoot component and its children. Placing Recoil-based components elsewhere will throw an error during run-time.

In order to make the CustomerInfo and CustomerDetails components work, we have to place them inside a RecoilRoot component. RecoilRoot must be imported first from the 'recoil' package.

The following is a top-level component called CustomerDemo that contains both these components as children -

import * as React from 'react';
import { RecoilRoot } from 'recoil';
import CustomerInfo from './CustomerInfo';
import CustomerDetails from './CustomerDetails';

// Function component containing both recoil-based components
export default function CustomerDemo() : JSX.Element {
    return (
        <RecoilRoot>
            <CustomerInfo />
            <CustomerDetails />
        </RecoilRoot>
    );
}

In the above example, both CustomerInfo and CustomerDetails components make use of a Recoil Atom and Selector. Any change that takes place in the custState atom, triggers a refresh in both these components. (Note: we have not written any code to make changes to the state. This can be easily done via a Form).

Using Recoil with SPFx-React Project

Using Recoil with a SPFx project is straightforward. First create a SPFx+React project and then install Recoil using npm.

Create a SPFx-React Project using Yeoman

yo @microsoft.sharepoint

In the wizard choose WebPart, React as the framework. Next wait for the creation of the project and npm packages to be downloaded.

Next install the Recoil library using the following npm command -

npm install recoil

Creating a Simple Temperature Conversion WebPart

For this article, we will be creating a simple Temperature conversion WebPart that stores the input temperature in Celcius as a Recoil Atom (number) and derive the converted value in Farenheit as a selector.

The temperature value will be stored in an Atom called temp defined in GlobalState.ts which is then imported into other components.

GlobalState.ts

This file defines an Atom called temp which stores the input temperature in Celcius. Components can use this Atom and also update it.

Next we define a selector called farenTemp which converts the value in the temp Atom (C) to Farenheit value. This is done in the get property.

Note that both atom and selector are functions that receive a JSON object with the settings.

// GlobalState.ts : Store all the Recoil Atoms & Selectors here
import { atom, RecoilState, selector } from "recoil";

// Atom to store Celcius temperature
export const temp : RecoilState<number> = atom<number>({
    key: 'tempState',
    default: 0
});

// Selector to convert C to F
export const farenTemp = selector({
    key: 'farenTempState',
    get: ({get}) => {
        const t = get(temp);

        return ((9/5) * t) + 32.0;
    }
})

Remember to drop this file under the components folder in the SPFx WebPart project.

Celcius.tsx

This component dislays an HTML Input and allows the user to enter a temperature value in Celcius. As the user types into the input field, the change event fires and is handled by an event-handler. Inside this event handler, the atom value is updated using the setCelcius function.

Notice how the [celcius,setCelcius] syntax is similar to the useState hook syntax.

// Celcius.tsx - Component to receive temperature input from user in Celcius
import * as React from 'react';
import styles from './RecoilDemo.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';

import { useRecoilState } from 'recoil';

import { temp } from './GlobalState';

export default function Celcius() : JSX.Element {

    const [celcius, setCelcius] = useRecoilState<number>(temp);

    return (
        <div>
            Enter Temp (C) : <input type="text" 
            onChange={ (ev : React.ChangeEvent<HTMLInputElement>)=> {
                const temp : string = ev.currentTarget.value;

                // Set the temp value into Global State atom (temp)
                setCelcius(parseFloat(temp));
            }} />
        </div>
    );
}

Farenheit.tsx

This is a straightforward component that makes use of the farenTemp selector and displays the converted temperature in Farenheit.

Whenever the user enters a new value in the Text field in the Celcius component, the Farenheit component is refreshed automatically as it has a dependency on the farenTemp selector which in turn derives its output from the temp atom.

// Farenheit.tax
import * as React from 'react';
import styles from './RecoilDemo.module.scss';
import { escape } from '@microsoft/sp-lodash-subset';

import { useRecoilValue } from 'recoil';

import { farenTemp } from './GlobalState';

export default function Farenheit() : JSX.Element {

    const faren = useRecoilValue<number>(farenTemp);

    return (
        <div>
            <h1>Temp in F : { faren }</h1>
        </div>
    );
}

RecoilDemo.tsx

In order for everything to fit together, we need a top-level component which will have the Celcius and Farenheit components as children. This component will wrap all the components inside a RecoilRoot tag for eveything to work.

This top-level component is loaded by the WebPart and rendered (WebPart code is omitted in this article for brevity. You can find the complete code in the GitHub repo for this article.

// RecoilDemo.ts - Top-level component loaded by RecoilDemoWebPart
import * as React from 'react';
import styles from './RecoilDemo.module.scss';
import { IRecoilDemoProps } from './IRecoilDemoProps';
import { escape } from '@microsoft/sp-lodash-subset';

import { RecoilRoot } from 'recoil';
import Celcius from './Celcius';
import Farenheit from './Farenheit';

export default class RecoilDemo extends React.Component<IRecoilDemoProps, {}> {
  public render(): React.ReactElement<IRecoilDemoProps> {
    const {
      description,
      isDarkTheme,
      environmentMessage,
      hasTeamsContext,
      userDisplayName
    } = this.props;


    return (
      <RecoilRoot>
        <section className={`${styles.recoilDemo} ${hasTeamsContext ? styles.teams : ''}`}>
          <div className={styles.welcome}>
            
            <h2>SPFx React with Recoil JS</h2>
            <div><strong>{escape(description)}</strong></div>
          </div>
          <div>
            <p>
              Following are two different React components that use common atoms / selectors
            </p>
            <Celcius />
            <Farenheit />
          </div>
        </section>
      </RecoilRoot>
    );
  }
}

How it Works

  • The RecoilDemoWebPart loads RecoilDemo.tsx React component
  • RecoilDemo.tsx loads RecoilRoot and hooks up the Celcius and Farenheit components with the atom and selector defined in GlobalState.tsx
  • When the user types a temperature value into the Text field in Celcius component, the temp Recoil atom is updated with the new value
  • This update triggers the farenTemp selector to be executed and subsequently all components using both the atom and the selectors are re-rendered

Running the example code

Clone the github repo to a folder. Open the folder in the command prompt and run the following command

    npm install

Once done run the follown command -

    gulp serve

Now open the online workbench in a browser window and add the RecoilDemo webpart. Enter any numeric value into the text box and watch the Farenheit equivalent displayed by another component.



WebPart project code

The SPFx WebPart code sample for this article can be found at -

https://github.com/mindsharein/spfx-recoil

Conclusion

Recoil is very easy to get started with. The hook based syntax for Atoms and Selectors is very intiutive to use, especially for beginners.

With Recoil, Facebook has provided a much needed alternative to Redux and MobX for state management in React projects.

No comments:

Post a Comment

Using Recoil with SPFx Try out the latest alternative to Redux with SPFx When developing with Redux or MobX, we have ha...