React-Recoil and React-Request

One of the best things about React is the huge developer community that exists creating an endless supply of libraries, component libraries and even full on frameworks to extend the use of React in web developmnet. In this lecture we'll target a few useful libraries that help solve some of Reacts greatest pain points.

What are React's Pain Points?

Below you'll see a list of common pain points and libraries that aim to solve them.

Pain Point Libraries
State Management Recoil, Mobx, Redux, XState, merced-react-hooks
Making API Calls react-query, react-request, merced-react-hooks
Forms Formik, merced-react-hooks
Styling Styled Components, Emotion, JSS

Why React Recoil?

Over the years Redux has been probably the most used State Management library for React, recently the creators of React, Facebook, released their own state management library which provides a much simpler and more "Reacty" library called Recoil. Since it's created by the makers of React it touts some very convinient and powerfel functionality.

Why react-query

React request doesn't just make the process of making API calls smooth, but it also abstracts away tedious issues like a caching and updating data, it has been growing quite popular since its release.

Let's Get Started

  • create a new react npx create-react-app recoilquery
  • install libraries npm install recoil react-query

Setting up the Providers

Like many react libraries (like React Router) we setup the library by wrapping our App in a provider.

  • The recoil provider will help deliver our state across our application
  • the react-query provider will help make the data from our fetch requests available across the app.

open up src/index.js

import React from "react";
import ReactDOM from "react-dom";
import "./index.css";
import App from "./App";
import reportWebVitals from "./reportWebVitals";
import { RecoilRoot } from "recoil";
import { QueryClient, QueryClientProvider } from "react-query";

const queryClient = new QueryClient();

ReactDOM.render(
  <QueryClientProvider client={queryClient}>
    <RecoilRoot>
      <React.StrictMode>
        <App />
      </React.StrictMode>
    </RecoilRoot>
  </QueryClientProvider>,
  document.getElementById("root")
);

// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint. Learn more: https://bit.ly/CRA-vitals
reportWebVitals();

Creating State with Recoil

In Recoil, state you want shared with Recoil are called atoms. Let's create an src/atom.js file to declare all our atoms.

import { atom } from "recoil";

// declare and export an atom
export const counterState = atom({
    // the key is used to track the state internally in recoil
    key: 'counterState',
    // default value is the value if not other value exists, the starting value essentially
    default: 0
})

Using the Recoil State

Let's create a counter component in src/components/counter.js

import { counterState } from "../atom";
import { useRecoilState } from "recoil";

function Counter(props) {
  // bring in the state from the atom
  const [counter, setCounter] = useRecoilState(counterState);

  return (
    <div>
      <h1>{counter}</h1>
      <button onClick={() => setCounter(counter + 1)}>Add</button>
    </div>
  );
}

export default Counter;

let's import the component in App and see it at work

src/App.js

import Counter from "./components/counter";

function App(props) {
  return (
    <div>
      <Counter />
    </div>
  );
}

export default App;

Now add two counters:

import Counter from "./components/counter";

function App(props) {
  return (
    <div>
      <Counter />
      <Counter />
    </div>
  );
}

export default App;

Notice they both always update, this is because they don't have their own internat state. Instead they are both working off the same external state from recoil. So recoil should be used when you have data that should be in sync throughout your app.

Using React-Query

create another component, src/components/request.js

import { useQuery } from "react-query";

function Request() {
  // make our query
  const response = useQuery("myQuery", async () => {
    const r = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    return r.json();
  });

  console.log(response);

  return <h1>Request</h1>;
}

export default Request;

the useQuery hook takes two arguments...

  • query key, this is a string for tracking and chaching the query. If another component uses the same key it knows it's the same query so won't refetch but instead use the cached data.
  • query function, a function that makes the desired request and returns a promise

Test it out in app

let's use this component in App.js

import Counter from "./components/counter";
import Request from "./components/request";

function App(props) {
  return (
    <div>
      <Counter />
      <Counter />
      <Request/>
    </div>
  );
}

export default App;

Examine the console.log and see how the response is given back.

Let's use two copies

import Counter from "./components/counter";
import Request from "./components/request";

function App(props) {
  return (
    <div>
      <Counter />
      <Counter />
      <Request/>
      <Request/>
    </div>
  );
}

export default App;

Notice that even though we used the component twice, the console.logs don't increase. This is cause both components are altering the data from the same place kinda like we saw in recoil.

What's in the response

The main things of note in the response object

  • data: the data from the api call
  • isLoading: a boolean on whether the call is still pending, can be used to return loading JSX
  • isError: a boolean on whether the call has failed to return error JSX
  • refetch: a function to repeat the call and update the data for everywhere it is used
  • error: the error if there is one

Bonus - Custom Hooks

Custom Hooks for react-query

To make it even easier to refer to the same api call in multiple components, you can build custom hooks to avoid typing the same query function key and function over and over agian. Make a file src/queryhooks.js.

queryhooks.js

import { useQuery } from "react-query";

// api request custom hook
export const useJsonPlaceholder = () => {
    // make api call and save response
    const response = useQuery("myQuery", async () => {
    const r = await fetch("https://jsonplaceholder.typicode.com/todos/1");
    return r.json();
  });
  // return response
  return response
};

now we can use this custom hook instead of typing out that entire useQuery again.

src/components/request.js

import { useJsonPlaceholder } from "../hooks";

function Request() {
  // make our query

  const response = useJsonPlaceholder()

  console.log(response);

  return <h1>Request</h1>;
}

export default Request;

Also, let's destructure some of those response properties and show how we can render the component conditionally.

import { useJsonPlaceholder } from "../hooks";

function Request() {
  // make our query

  const { data, isError, isLoading, refetch } = useJsonPlaceholder();

  // JSX for ERROR
  if (isError) {
    return (
      <div>
        <h1>Request Failed</h1>
        <button onClick={() => refetch()}>Try Again</button>
      </div>
    );
  }

  // JSX for Loading
  if (isLoading) {
    return (
      <div>
        <h1>Loading</h1>
        <button onClick={() => refetch()}>Try Again</button>
      </div>
    );
  }

  // JSX for API Call Complete
  return (
    <div>
      <h1>Request Succeded</h1>
      <ul>
        {Object.keys(data).map((key) => (
          <li>
            {key}: {data[key]}
          </li>
        ))}
      </ul>
      <button onClick={() => refetch()}>Try Again</button>
    </div>
  );
}

export default Request;

custom hooks for recoil

For our atoms we can just make our custom hooks when we declare them, like so.

atom.js

import { atom, useRecoilState } from "recoil";

// declare and export an atom
const counterState = atom({
    // the key is used to track the state internally in recoil
    key: 'counterState',
    // default value is the value if not other value exists, the starting value essentially
    default: 0
})

// declare custom hook for the using the atom
export const useCounterState = () => {
    return useRecoilState(counterState)
}

Now we don't have to import useRecoilState and the atom, we can just import the custom hook in any components that use that state.

counter.js

import { useCounterState } from "../atom";

function Counter(props) {
  // bring in the state from the atom
  const [counter, setCounter] = useCounterState()

  return (
    <div>
      <h1>{counter}</h1>
      <button onClick={() => setCounter(counter + 1)}>Add</button>
    </div>
  );
}

export default Counter;

The great thing about customhooks it lets us clean up our code and make reusing state across our app so much easier!

Non-Deliverable Lab

Try to take the build you created last week and refactor it using react-query and/or recoil.

I highly recommend making these changes on a branch so you keep your original code. Commit what you have already and then run the command git checkout -b refactor to work from a branch called refactor. While on this branch push your code using git push origin refactor.

  • you can switch branches at anytime with git checkout BRANCH_NAME
  • you can see what your current list of branches with the command of git branch