Open6

Jotaiについて勉強する

KeiKei

はじめに

Reactの状態管理ライブラリである、Jotaiを学習してみようと思う。
とりあえずこのスレッドがJotaiの基本的な使い方を学ぶのをゴールとする。
https://jotai.org/

KeiKei

特徴

  • グローバルな状態管理
  • 最小限なAPI
  • 不要な再レンダリングは自動的に排除

Jotai has a very minimal API and is TypeScript oriented. It is as simple to use as React’s integrated useState hook, but all state is globally accessible, derived state is easy to implement, and unnecessary re-renders are automatically eliminated.

import { atom, useAtom } from 'jotai'

// Create your atoms and derivatives
const textAtom = atom('hello')
const uppercaseAtom = atom(
  (get) => get(textAtom).toUpperCase()
)

// Use them anywhere in your app
const Input = () => {
  const [text, setText] = useAtom(textAtom)
  const handleChange = (e) => setText(e.target.value)
  return (
    <input value={text} onChange={handleChange} />
  )
}

const Uppercase = () => {
  const [uppercase] = useAtom(uppercaseAtom)
  return (
    <div>Uppercase: {uppercase}</div>
  )
}

// Now you have the components
const App = () => {
  return (
    <>
      <Input />
      <Uppercase />
    </>
  )
}
KeiKei

Create Atom

まずプリミティブと派生アトムを作り、状態を構築する。

First create primitive and derived atoms to build state.

Primitive atoms

プリミティブアトムは、boolean, number, string, object, arrayなどどんな型でもよい。

import { atom } from 'jotai'

const countAtom = atom(0)

const countryAtom = atom('Japan')

const citiesAtom = atom(['Tokyo', 'Kyoto', 'Osaka'])

const animeAtom = atom([
  {
    title: 'Ghost in the Shell',
    year: 1995,
    watched: true
  },
  {
    title: 'Serial Experiments Lain',
    year: 1998,
    watched: false
  }
])

Derived atoms

派生アトムは、自身の値を返す前に、別のアトムから読み取り可能

const progressAtom = atom((get) => {
  const anime = get(animeAtom)
  return anime.filter((item) => item.watched).length / anime.length
})
KeiKei

Use atoms

アトム作成後、コンポーネント内でアトムを使ってstateを読み書きする

Read and write form same component

同じコンポーネント内でアトムの読み取りと書き込みの両方を行う場合は、useAtomフックを組み合わせて使用すると簡単。

import { useAtom } from 'jotai'

const AnimeApp = () => {
  const [anime, setAnime] = useAtom(animeAtom)

  return (
    <>
      <ul>
        {anime.map((item) => (
          <li key={item.title}>{item.title}</li>
        ))}
      </ul>
      <button onClick={() => {
        setAnime((anime) => [
          ...anime,
          {
            title: 'Cowboy Bebop',
            year: 1998,
            watched: false
          }
        ])
      }}>
        Add Cowboy Bebop
      </button>
    <>
  )
}

Read and write from separate components

アトム値の読み取りまたは書き込みのみを行う場合は、
useAtomValueフックとuseSetAtomフックを個別に使用して再レンダリングを最適化を行う。

import { useAtomValue, useSetAtom } from 'jotai'

const AnimeList = () => {
  const anime = useAtomValue(animeAtom)

  return (
    <ul>
      {anime.map((item) => (
        <li key={item.title}>{item.title}</li>
      ))}
    </ul>
  )
}

const AddAnime = () => {
  const setAnime = useSetAtom(animeAtom)

  return (
    <button onClick={() => {
      setAnime((anime) => [
        ...anime,
        {
          title: 'Cowboy Bebop',
          year: 1998,
          watched: false
        }
      ])
    }}>
      Add Cowboy Bebop
    </button>
  )
}

const ProgressTracker = () => {
  const progress = useAtomValue(progressAtom)

  return (
    <div>{Math.trunc(progress * 100)}% watched</div>
  )
}

const AnimeApp = () => {
  return (
    <>
      <AnimeList />
      <AddAnime />
      <ProgressTracker />
    </>
  )
}
KeiKei

サーバーサイドレンダリング

フレームワーク等でサーバーサイドレンダリングを行う際には、Providerコンポーネントを用意する。

import { Provider } from 'jotai'

// Placement is framework-specific (see below)
<Provider>
  {...}
</Provider>

Next.js (app directory)

  1. プロバイダーを別のクライアントコンポーネントに作成
  2. プロバイダをルートのlayout.js にインポートする
// providers.js (app directory)
'use client'

import { Provider } from 'jotai'

export default function Providers({ children }) {
  return (
    <Provider>
      {children}
    </Provider>
  )
}


// layout.js (app directory)
import Providers from './providers'

export default function RootLayout({ children }) {
  return (
    <html lang="en">
      <body>
        <Providers>
          {children}
        </Providers>
      </body>
    </html>
  )
}

Next.js (pages directory)

  • _app.jsにプロバイダー作成
// _app.js (pages directory)
import { Provider } from 'jotai'

export default function App({ Component, pageProps }) {
  return (
    <Provider>
      <Component {...pageProps} />
    </Provider>
  )
}