npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2024 – Pkg Stats / Ryan Hefner

@hatchifyjs/react-rest

v0.1.38

Published

- [What is react-rest](#what-is-react-rest?) - [TypeScript](#typescript) - [Relationships](#relationships) - [Focus on UI](#focus-on-ui) - [Building a Simple Todo App with react-rest](#building-a-simple-todo-app-with-react-rest) - [Project Setup](

Downloads

467

Readme

react-rest

What is react-rest?

React-rest is a model-driven library for interacting with your backend. By defining the schemas (AKA models) of your backend resources, react-rest will provide you with a set of hooks and functions that you can use across your React app.

TypeScript

React-rest provides TypeScript support. Here's an example of how two schemas (Todo and User) provide auto-completion for an instantiated react-rest app (hatchedReactRest):

react-rest TypeScript

Relationships

By providing react-rest your schemas, it will be able to fetch related data from your JSON:API backend.

Note: take note of code commented with the 👀 emoji.

const [todos] = hatchedReactRest.Todo.useAll({ include: ["user"] })

todos[0].name
todos[0].user.name // 👀

Focus on UI

React-rest allows you to keep your focus on your UI. By defining your schemas ahead of time, you will not have to worry about your REST layer. Any changes you make to your data will trigger re-fetches in react-rest so that you’re always up to date.

Creating a todo and updating list

Building a Simple Todo App with react-rest

The following guide creates a simple Todo app from scratch. At first, it will look like this:

Todo app without relationships

Then, we'll extend the app to handle relationships:

Todo app with relationships

Project Setup

Note: the ✏️ icon indicates when to follow along!

✏️ Perform the following steps:

  1. Ensure you’re using node 18 and npm 9.

    node -v
  2. Create a new react app titled “react-rest-app” with Vite. TypeScript is recommended, but not required:

    npm create vite@latest react-rest-app -- --template react-ts
  3. Move into the /react-rest-app directory and install the node modules:

    cd react-rest-app
    npm install
  4. Install the @hatchifyjs/react-rest and @hatchify-js/rest-client-jsonapi libraries:

    npm install @hatchifyjs/react-rest @hatchifyjs/rest-client-jsonapi
  5. For this guide, we’re going to use mock service worker as our backend.

    a. Install msw:

    npm install msw --save-dev

    b. Create a /mocks directory and initialize msw.

    mkdir src/mocks
    npx msw init public/ --save

    c. Run the following command to add a browser.js file to your /mocks directory and populate it with some request handlers and mocked data:

    curl -o src/mocks/browser.ts https://raw.githubusercontent.com/bitovi/hatchify/main/example/react-rest/src/mocks/browser.ts
  6. To start the mock service worker, replace the contents of src/main.tsx with the following:

    import React from "react"
    import ReactDOM from "react-dom/client"
    import App from "./App.tsx"
    
    // 👀
    if (process.env.NODE_ENV === "development") {
      await import("./mocks/browser").then(({ worker }) => {
        worker.start()
      })
    }
    
    ReactDOM.createRoot(document.getElementById("root") as HTMLElement).render(
      <React.StrictMode>
        <App />
      </React.StrictMode>,
    )
  7. Run npm run dev, and open http://localhost:3000/ to see the starter app in action.

image

Listing, creating, and deleting todos

The following makes a simple todo app that loads todos from /api/todos and allows the user to create and delete todos.

✏️ Replace the contents of src/App.tsx with the following:

// App.tsx
import { useState } from "react"
import type { Schema } from "@hatchifyjs/react-rest"
import hatchifyReactRest from "@hatchifyjs/react-rest"
import createClient from "@hatchifyjs/rest-client-jsonapi"

const Todo: Schema = {
  name: "Todo",
  ui: { displayAttribute: "name" },
  attributes: {
    name: "string",
  },
}

const jsonapi = createClient("/api", { Todo })

const hatchedReactRest = hatchifyReactRest(jsonapi)

function App() {
  const [todos, todosMeta] = hatchedReactRest.Todo.useAll()
  const [createTodo, createState] = hatchedReactRest.Todo.useCreateOne()
  const [deleteTodo, deleteState] = hatchedReactRest.Todo.useDeleteOne()
  const [todoName, setTodoName] = useState("")

  if (todosMeta.isPending) {
    return <div>loading...</div>
  }

  return (
    <div>
      <div>
        <input type="text" value={todoName} onChange={(e) => setTodoName(e.target.value)} />
        <button
          disabled={createState.isPending}
          type="button"
          onClick={() => {
            createTodo({ attributes: { name: todoName } })
            setTodoName("")
          }}
        >
          {createState.isPending ? "submitting..." : "submit"}
        </button>
      </div>
      <table>
        <thead>
          {todos.map((todo) => (
            <tr key={todo.id}>
              <td>{todo.name}</td>
              <td>
                <button disabled={deleteState.isPending} type="button" onClick={() => deleteTodo(todo.id)}>
                  delete
                </button>
              </td>
            </tr>
          ))}
        </thead>
      </table>
    </div>
  )
}

export default App

✏️ Make sure to save the file. Your browser should automatically update with the changes.

In the following sections, we'll configure react-rest to understand the data it’s loading, where it’s loading it from, and that it’s using a JSON:API data format.

Note: In your Vite app, you may have noticed the duplicated GET requests in the network tab. This is happening because Vite runs React in strict mode in development. In strict mode, React is running hooks twice (hence the duplicated requests) to help catch bugs in our code. You can read more about it here.

Configuration

Schemas

React-rest is powered by schema-driven development. A schema is an object representation of our backend resources. React-rest provides a Schema type that we can follow to make sure our objects are defined correctly. In our App.tsx we have created a Todo schema, and on it we defined the following:

  • The name of our schema, Todo

  • The primary display attribute, in this case name

  • An object containing all the attributes of our Todo, currently only name

// App.tsx
import type { Schema } from "@hatchifyjs/react-rest"

// ...

const Todo: Schema = {
  name: "Todo",
  ui: { displayAttribute: "name" },
  attributes: {
    name: "string",
  },
}

REST Client

React-rest expects a client to be passed into it. HatchifyJS provides a JSON:API rest client to use which takes in a base URL, and a mapping of schemas to endpoints. The response from the createClient call is what we will pass into our react-rest app.

// App.tsx
import createClient from "@hatchifyjs/rest-client-jsonapi"

// ...

const jsonapi = createClient("/api", { Todo })

hatchifyReactRest
// App.tsx
import hatchifyReactRest from "@hatchifyjs/react-rest"

// ...

const hatchedReactRest = hatchifyReactRest(jsonapi)

Fetching a list

To fetch a list of todos we are using the useAll hook from react-rest inside of our App.tsx. The object returned from hatchifyReactRest will contain a key for each schema passed into it. Off of that key we can call the useAll hook which will fetch us a list of todos. Any time a user mutates a todo (by creating, updating, or deleting), our useAll hook will re-fetch the latest data.

The return from useAll is an array where the first index contains an array of todos and the second index contains any metadata about our request, such as whether we’re fetching data or if we received an error.

// App.tsx: App component
// 👀
const [todos, todosMeta] = hatchedReactRest.Todo.useAll()

// 👀
if (todosMeta.isPending) {
  return <div>loading...</div>
}

// 👀
return (
  <table>
    <thead>
      {todos.map((todo) => (
        <tr key={todo.id}>
          <td>{todo.name}</td>
        </tr>
      ))}
    </thead>
  </table>
)

Note: You might be unfamiliar with calling a hook as a method on an object (like we're doing above) but it's perfectly valid! A hook is a hook, and it still follows the rules of hooks, even when it's a method.

Creating

To create new todos we are using the useCreateOne hook from react-rest. The useCreateOne hook returns an array with 3 values. The first index returns a mutate function, which is what we use to create a todo. The second index, similar to useAll, contains metadata about our create request. Finally, the third index contains the latest created todo; which in this example we can safely ignore.

// App.tsx: App component
const [todos, todosMeta] = hatchedReactRest.Todo.useAll()
// 👀
const [createTodo, createState] = hatchedReactRest.Todo.useCreateOne()
const [todoName, setTodoName] = useState("")

// 👀
if (todosMeta.isPending) {
  return <div>loading...</div>
}

// 👀
return (
  <div>
    <div>
      {/* 👀 */}
      <input type="text" value={todoName} onChange={(e) => setTodoName(e.target.value)} />
      {/* 👀 */}
      <button disabled={createState.isPending} type="button" onClick={() => createTodo({ attributes: { name: todoName } })}>
        {createState.isPending ? "submitting..." : "submit"}
      </button>
    </div>
    <table>
      <thead>
        {todos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.name}</td>
          </tr>
        ))}
      </thead>
    </table>
  </div>
)

Deleting

For deleting todos, our App.tsx is using useDeleteOne. This hook behaves similarly to useCreateOne: the first index contains the delete function and the second contains metadata about our delete request. This hook does not return a third index as useCreateOne does.

// App.tsx: App component
const [todos, todosMeta] = hatchedReactRest.Todo.useAll()
const [createTodo, createState] = hatchedReactRest.Todo.useCreateOne()
// 👀
const [deleteTodo, deleteState] = hatchedReactRest.Todo.useDeleteOne()
const [todoName, setTodoName] = useState("")

if (todosMeta.isPending) {
  return <div>loading...</div>
}

return (
  <div>
    <div>
      <input type="text" value={todoName} onChange={(e) => setTodoName(e.target.value)} />
      <button disabled={createState.isPending} type="button" onClick={() => createTodo({ attributes: { name: todoName } })}>
        {createState.isPending ? "submitting..." : "submit"}
      </button>
    </div>
    <table>
      <thead>
        {todos.map((todo) => (
          <tr key={todo.id}>
            <td>{todo.name}</td>
            {/* 👀 */}
            <td>
              <button disabled={deleteState.isPending} type="button" onClick={() => deleteTodo(todo.id)}>
                delete
              </button>
            </td>
          </tr>
        ))}
      </thead>
    </table>
  </div>
)

Relationships: Allowing a user to be assigned to todos

If we define many schemas and the relationships between them, then we can fetch the related records using the hooks and functions provided by react-rest.

Fetching todos with related users

To fetch todos with their related users, we need to make a few changes to our App.tsx.

First, we need to update our Todo schema to have a relationship to a user and then create a new User schema.

✏️ Modify the Todo schema to have a relationships property:

// App.tsx
const Todo: Schema = {
  name: "Todo",
  ui: { displayAttribute: "name" },
  attributes: {
    name: "string",
  },
  // 👀
  relationships: {
    user: {
      type: "one",
      schema: "User",
    },
  },
}

✏️ Create a new User schema:

// App.tsx
// 👀
const User: Schema = {
  name: "User",
  ui: { displayAttribute: "name" },
  attributes: {
    name: "string",
  },
  relationships: {
    todos: {
      type: "many",
      schema: "Todo",
    },
  },
}

Next, we need to pass the new User schema into createClient and hatchifyReactRest.

✏️ Pass the new schema into createClient and hatchifyReactRest:

// App.tsx
const jsonapi = createClient("/api", {
  Todo,
  // 👀
  User,
})

const hatchedReactRest = hatchifyReactRest(jsonapi)

Finally, we update our useAll hook to include user data and update our list to print a user name for each todo.

✏️ Update the useAll hook to include user data:

// App.tsx: App component
const [todos, todosMeta] = hatchedReactRest.Todo.useAll({ include: ["user"] }) // 👀

✏️ Update the list to print a user name for each todo:

// App.tsx: App component
{
  todos.map((todo) => (
    <tr key={todo.id}>
      <td>{todo.name}</td>
      {/* 👀 */}
      <td>{todo.user?.name}</td>
      <td>
        <button disabled={deleteState.isPending} type="button" onClick={() => deleteTodo(todo.id)}>
          delete
        </button>
      </td>
    </tr>
  ))
}

Your App.tsx should now look like this:

// App.tsx
import { useState } from "react"
import type { Schema } from "@hatchifyjs/react-rest"
import hatchifyReactRest from "@hatchifyjs/react-rest"
import createClient from "@hatchifyjs/rest-client-jsonapi"

const Todo: Schema = {
  name: "Todo",
  ui: { displayAttribute: "name" },
  attributes: {
    name: "string",
  },
  relationships: {
    user: {
      type: "one",
      schema: "User",
    },
  },
}

const User: Schema = {
  name: "User",
  ui: { displayAttribute: "name" },
  attributes: {
    name: "string",
  },
  relationships: {
    todos: {
      type: "many",
      schema: "Todo",
    },
  },
}

const jsonapi = createClient("/api", { Todo, User })

const hatchedReactRest = hatchifyReactRest(jsonapi)

function App() {
  const [todos, todosMeta] = hatchedReactRest.Todo.useAll({ include: ["user"] })
  const [createTodo, createState] = hatchedReactRest.Todo.useCreateOne()
  const [deleteTodo, deleteState] = hatchedReactRest.Todo.useDeleteOne()
  const [todoName, setTodoName] = useState("")

  if (todosMeta.isPending) {
    return <div>loading...</div>
  }

  return (
    <div>
      <div>
        <input type="text" value={todoName} onChange={(e) => setTodoName(e.target.value)} />
        <button
          disabled={createState.isPending}
          type="button"
          onClick={() => {
            createTodo({ attributes: { name: todoName } })
            setTodoName("")
          }}
        >
          {createState.isPending ? "submitting..." : "submit"}
        </button>
      </div>
      <table>
        <thead>
          {todos.map((todo) => (
            <tr key={todo.id}>
              <td>{todo.name}</td>
              <td>{todo.user?.name}</td>
              <td>
                <button disabled={deleteState.isPending} type="button" onClick={() => deleteTodo(todo.id)}>
                  delete
                </button>
              </td>
            </tr>
          ))}
        </thead>
      </table>
    </div>
  )
}

export default App

And your app should now look like this:

Fetching users and populating a select

Now that we've added the User schema into our createClient and hatchifyReactRest, we can fetch users and populate a select with them.

✏️ Add a new useAll hook to fetch users:

// App.tsx: App component
const [users, usersMeta] = hatchedReactRest.User.useAll()

✏️ Add a stateful select input that has users as options:

// App.tsx: App component
const [selectedUser, setSelectedUser] = useState("")
// App.tsx: App component
<input
  type="text"
  value={todoName}
  onChange={(e) => setTodoName(e.target.value)}
/>
{/* 👀 */}
<select
  disabled={usersMeta.isPending}
  value={selectedUser}
  onChange={(e) => setSelectedUser(e.target.value)}
>
  <option value="">select user</option>
  {users.map((user) => (
    <option key={user.id} value={user.id}>
      {user.name}
    </option>
  ))}
</select>
<button
  disabled={createState.isPending}
  type="button"
  onClick={() => {
    createTodo({
      attributes: { name: todoName },
    })
    setTodoName("")
  }}
>
  {createState.isPending ? "submitting..." : "submit"}
</button>

Creating a todo with a user

Now that we have a select populated with users, we can create a todo with a user.

✏️ Update createTodo to pass in the selected user id and set the selectedUser back to its default state:

// App.tsx: App component
<button
  disabled={createState.isPending}
  type="button"
  onClick={() => {
    createTodo({
      attributes: { name: todoName },
      // 👀
      relationships: { user: { id: selectedUser } },
    })
    setTodoName("")
    // 👀
    setSelectedUser("")
  }}
>
  {createState.isPending ? "submitting..." : "submit"}
</button>

Altogether, our App.tsx should look like this:

// App.tsx
import { useState } from "react"
import type { Schema } from "@hatchifyjs/react-rest"
import hatchifyReactRest from "@hatchifyjs/react-rest"
import createClient from "@hatchifyjs/rest-client-jsonapi"

const Todo: Schema = {
  name: "Todo",
  ui: { displayAttribute: "name" },
  attributes: {
    name: "string",
  },
  relationships: {
    user: {
      type: "one",
      schema: "User",
    },
  },
}

const User: Schema = {
  name: "User",
  ui: { displayAttribute: "name" },
  attributes: {
    name: "string",
  },
  relationships: {
    todos: {
      type: "many",
      schema: "Todo",
    },
  },
}

const jsonapi = createClient("/api", { Todo, User })

const hatchedReactRest = hatchifyReactRest(jsonapi)

function App() {
  const [todos, todosMeta] = hatchedReactRest.Todo.useAll({ include: ["user"] })
  const [createTodo, createState] = hatchedReactRest.Todo.useCreateOne()
  const [deleteTodo, deleteState] = hatchedReactRest.Todo.useDeleteOne()
  const [todoName, setTodoName] = useState("")

  const [users, usersMeta] = hatchedReactRest.User.useAll()
  const [selectedUser, setSelectedUser] = useState("")

  if (todosMeta.isPending) {
    return <div>loading...</div>
  }

  return (
    <div>
      <div>
        <input type="text" value={todoName} onChange={(e) => setTodoName(e.target.value)} />
        <select disabled={usersMeta.isPending} value={selectedUser} onChange={(e) => setSelectedUser(e.target.value)}>
          <option value="">select user</option>
          {users.map((user) => (
            <option key={user.id} value={user.id}>
              {user.name}
            </option>
          ))}
        </select>
        <button
          disabled={createState.isPending}
          type="button"
          onClick={() => {
            createTodo({
              attributes: { name: todoName },
              relationships: { user: { id: selectedUser } },
            })
            setTodoName("")
            setSelectedUser("")
          }}
        >
          {createState.isPending ? "submitting..." : "submit"}
        </button>
      </div>
      <table>
        <thead>
          {todos.map((todo) => (
            <tr key={todo.id}>
              <td>{todo.name}</td>
              <td>{todo.user?.name}</td>
              <td>
                <button disabled={deleteState.isPending} type="button" onClick={() => deleteTodo(todo.id)}>
                  delete
                </button>
              </td>
            </tr>
          ))}
        </thead>
      </table>
    </div>
  )
}

export default App

Congratulations! You've just created an app with react-rest with a fully functional development backend, complete with CRUD operations and relationship support.

Note: At this point we've reached the limits of what our todo app's msw-powered API supports. If you would like to continue exploring the HatchifyJS ecosystem, then we suggest setting up a standalone HatchifyJS backend. The getting started guide for a HatchifyJS backend can be found here.

For more information on react-rest, read on.

Alternatives to hooks

Sometimes we need to fetch data outside of our React components. For these cases, react-rest provides us with promise-based equivalents.

| hooks | promises | | ------------ | --------- | | useCreateOne | createOne | | useUpdateOne | updateOne | | useDeleteOne | deleteOne | | useAll | findAll | | useOne | findOne |