かもメモ

自分の落ちた落とし穴に何度も落ちる人のメモ帳

React 👻 jotai を使うと localStorage を使った永続化が簡単だった件について

SPA はリロードすると state が消えてしまうので永続化したい状態ってのが結構あります
生の React だと カスタム hook や useEffect 内で localStorage にアクセスして保存させたえい、Recoil でも effect を使って永続化するコードを書く必要がありました
jotai を使うと localStorage に保存するための API が用意されていて永続化がめちゃくちゃ簡単だったのでメモに残しておきます

環境

  • React 18.2.0
  • Next.js 13.2.4
  • jotai 2.0.3
  • TypeScript 5.0.2

atomWithStorage で直接 localStorage に state を保存できる!

atomWithStorage(key, initialValue, storage)

  • key (required): a unique string used as the key when syncing state with localStorage, sessionStorage, or AsyncStorage
  • initialValue (required): the initial value of the atom
  • storage (optional): an object with:
    • getItem, setItem and removeItem methods for storing/retrieving/deleting persisted state; defaults to using localStorage for storage/retrieval and JSON.stringify()/JSON.parse() for serialization/deserialization.
    • Optionally, the storage has a subscribe property, which can be used to synchronize storage. The default localStorage handles storage events for cross-tab synchronization.

cf. Storage — Jotai, primitive and flexible state management for React

auth.atom.ts

import { atomWithStorage } from 'jotai/utils';
export const isLoggedIn = true as const;
export const unLoggedIn = false as const;
const beforeLoginCheck = undefined;
type AuthType = typeof isLoggedIn | typeof unLoggedIn | typeof beforeLoginCheck;
// "auth" という key で localStorage に保存される
// 保存される値は自動的に JSON.stringify() される
export const authAtom = atomWithStorage<AuthType>('auth', beforeLoginCheck);

localStorage に auth という key で保存する state を作成

useAuth.ts

import { useCallback } from 'react';
import { useAtomValue, useSetAtom } from 'jotai';
import { RESET } from 'jotai/utils';
import { authAtom, isLoggedIn, unLoggedIn } from './auth.atom';

export const useAuth = () => {
  return useAtomValue(authAtom);
};

export const useAuthMutators = () => {
  const setAuth = useSetAtom(authAtom);
  
  const clearAuth = useCallback(() => {
    // RESET を渡すと localStorage から削除される
    setAuth(RESET);
  }, [setAuth]);

  const login = useCallback(() => {
    setAuth(isLoggedIn);
  }, [setAuth]);

  const loginError = useCallbacl(() => {
    setAuth(unLoggedIn);
  }, [setAuth]);
  
  return {clearAuth, login, loginError};
};

useAuth() は localStorage から取り出した値を返すだけの hook。
※ localStorage に値が存在しない場合は atom 作成時の initialValue が返される

useAuthMutators() は localStorage から delteItem で永続化した state を完全に削除しうてしまう clearAuth と 値を更新して localStorage に保存する loginloginError を提供するようにした

コンポーネントでの使用
import { useCallback } from 'react';
import { useAuthMutators } from './useAuth';

function LogoutButton(): JSX.Element {
  const { clearAuth } = useAuthMutators();
  const handleLogout = useCallback(() => {
    clearAuth()
  }, [clearAuth]);

  return <button type="button" onClick={handleLogout}>Logout</button>
}

function LoginForm(): JSX.Element {
  const { login, loginError } = useAuthMutators();
  const handleSubmit = useCallback(async (evt: FormEvent<HTMLFormElement>) => {
    evt.preventDefault();
    // 略
    try {
      const isLogin = await postLoginForm(data);
      login();
    } catch (error) {
      loginError();
    }
  }, [login, loginError]);

  return <form onSubmit={handleSubmit}>{/*略*/}</form>;
}

export { LogoutButton, LoginForm };

👇

import { useAuth } from './useAuth';
import { LoginForm, LogoutButton } from './MyComponents';

function MyApp(): JSX.Element {
  const auth = useAuth();  
  if ( !auth ) {
    return <LoginForm />;
  }
  
  return <LogoutButton />;
}

ログインに成功すると localStorage に {"auth": "true"} が保存され、ログアウトすると localStorage から state が削除される

jotai atomWithStorage sample

所管

jotai を使えば localStorage にアクセスする部分を意識することなく localStorage で状態の永続化ができました!
また、リロードの瞬間など localStorage にアクセスできない時は atom 作成時の initialValue が返されるようになっていて Next.js で使う場合も localStorage にアクセスできないサーバーサイドを意識することなくそのまま使うことができました
initialValue を工夫したり <Suspense> と併用することでリロード時だけ loader を挟むなどもできそうです!

👻 jotai のメモ


[参考]

Tips さくらのレンタルサーバーに公開鍵で ssh 接続したい

WordPress の host とかでお安いから使い続けてるみんな大好きさくらのレンタルサーバー!
SSH 接続できるのですが毎回パスフレーズ入力するのなんだかな〜と思ってたら公開鍵が使えるっぽいのでやってみたのメモ (PC買い替えたらまた設定するだろうからね…)

手順

  1. 公開鍵の作成
  2. さくらのレンタルサーバーに公開鍵をアップロード
  3. Host の設定

1. 公開鍵の作成

id_ed25519_sakura でキーを作成
パスフレーズも設定した

$ ssh-keygen -t ed25519 -C "your_email@example.com" -t ~/.ssh/id_ed25519_sakura
Enter passphrase (empty for no passphrase):

~/.ssh ディレクトリに ssh 公開鍵と秘密鍵ができていることを確認

  • id_ed25519_sakura
  • id_ed25519_sakura.pub

2. さくらのレンタルサーバーに公開鍵をアップロード

  1. さくらサーバーに ~/.ssh/authorized_keys として公開鍵をアップロードする
  2. authorized_keysパーミッションを 600 にする

※ 既に authorized_keys が存在する場合は公開鍵の情報を追記しても OK っぽい

コマンドでアップロードする方法

$ scp ~/.ssh/id_ed25519_sakura.pub {user}@{user}.sakura.ne.jp:./.ssh/authorized_keys

公開鍵を使った ssh 接続の確認

$ ssh {user}@{user}.sakura.ne.jp -i ~/.ssh/id_ed25519_sakura
% Welcome to FreeBSD!

サーバーにログインできていれば OK

3. Host の設定

毎回長いコマンドを打つのが面倒なので Host 設定をして ssh 接続のエイリアスを作成する
~/.ssh/config に Host の設定を追加

Host sakura
  HostName {user}.sakura.ne.jp
  User {user}
  Port 22
  UseKeychain yes
  IdentityFile ~/.ssh/id_ed25519_sakura

設定を作成したら ssh sakura でさくらサーバーに ssh 接続ができるようになる

おわり ₍ ᐢ. ̫ .ᐢ ₎


cf.

chaika.hatenablog.com

React TypeScript vanilla-extract で !important が使いたい!

vanilla-extract を使っていてライブラリが element に直接出力する CSS をどうしても上書きする必要があり !important を使おうとしたのですが TypeScript の型でハマったのでメモ

vanilla-extract を使って !important を使う

ライブラリの出力 CSS を上書きするために !import を指定するとセレクタの箇所で type error になる

import { globalStyle } from '@vanilla-extract/css';

globalStyle('#lib-container', {
  position: 'relative !important',
});

=> (property) position?: CSSVarFunction | Property.Position | (CSSVarFunction | Property.Position | undefined)[] | undefined The position CSS property sets how an element is positioned in a document. The top, right, bottom, and left properties determine the final location of positioned elements.

as でキャストしてしまえば OK

むりやり感はあるが as でキャストすれば type error にならず、問題なく !importantCSS が出力される

import { globalStyle } from '@vanilla-extract/css';

globalStyle('#lib-container', {
- position: 'relative !important',
+ position: 'relative !important' as 'relative',
});

何でキャストするかは CSS のプロパティで変える必要がある

CSS を書く時に !important はカオスを産む根源だと思っているので、どうしてもという場合を除いて使わないほうが良いと考えています。 今回の方法はあまりお行儀が良くないと思うが、どうしても必要な場合は as でキャストすれば TypeScript な vanilla-extract でも問題なく !important が使えることが解りました。

たぶんドキュメントにも書かれてない気がする (!important 使わない設計になるよね。って思想だと思うのでそれはそう)


[参考]