React備忘録(その2)

目次
  1. スタイル
  2. Domの操作
    1. useRef
    2. forwardRef
    3. createPortal
  3. Redux Toolkit
    1. Reduxについて
    2. Redux Toolkitを使ってみる
    3. Redux Thunk

スタイル

インラインスタイル
*style属性を要素のスタイリングの主要な手段として使うことは一般的に推奨されません
疑似クラス・疑似要素・メディアクエリが使えません

// 数値には自動的にpxを付け加えます
<div style={{ height: 10, color: 'blue', backgroundImage: 'url(' + imgUrl + ')' }}>
  Hello World!
</div>

Pure CSS
*スコープが広いのでクラス名での衝突が起きやすい

create-react-appした時にApp.cssにはデフォルトで適用されています
コンポーネントごとにcssファイルを作成して読み込みます

import './Hoge.css';

 <div className="class名" />

CSS Modules

create-react-appで導入されているので、すぐに使えます
スコープはコンポーネント単位になりクラス名の衝突が起きません

コンポーネントごとに読み込むcssファイル名には「.module.css」をつけます

//stylesにはキーがclass名、値が一意のclass名のオブジェクトが渡ってきます
import styles from "./Hoge.module.css"; 

 <div className={styles.class名}>
//class名にハイフンがある場合
 <div className={styles["class名"]}>

CSS in JS
*ライブラリ(styled-components・emotionなど)を導入する必要があります

CSSをJSファイルの中で記述します
ファイルが一つで済み、propsにより動的にスタイルを変更できます

styled-components

npm install --save styled-components
import styled from 'styled-components';

const コンポーネント名 = Module名.要素`
 適用するcss
`
const Button = styled.button`
  /* propsにより動的にスタイル */
  background: ${props => props.primary ? "red" : "white"};
  color: ${props => props.primary ? "white" : "red"};

  font-size: 1em;
  margin: 1em;
  padding: 0.25em 1em;
  border: 2px solid red;
  border-radius: 3px;
`;

// 継承する
const TomatoButton = styled(Button)`
  color: tomato;
  border-color: tomato;
`;

 <div>
    <Button>Normal</Button>
    <Button primary>Primary</Button>
   <TomatoButton>Tomato Button</TomatoButton>
 </div>

CSSフレームワーク
*コンポーネントが用意されているタイプとクラスを適応するタイプがあります

  • ChakraUI・Material-UIなど:コンポーネントが用意されているタイプ
  • Bootstrap・Tailwind CSSなど:クラスを適応するタイプ

Domの操作

useRef

関数コンポーネントでは「useRef」を使ってDom要素の参照ができます(クラスコンポーネントのref属性)
「useRef」はコンポーネント内での値を保持できます(通常の変数はレンダリングの度に初期化されるので、保持したい場合は「useRef」か「useState」を使います)

  • 「useRef」では値が更新されてもコンポーネントの再レンダリングが発生しません(「useState」はstateの値が変更される度に再レンダリングします)
  • 「currentプロパティの値」は再レンダリングが発生しても初期化されません
  • 「refオブジェクト」をJSXのref属性に渡すとそのDOMにアクセスできます

DOMの参照例
「useRef」で「refオブジェクト」を作成
取得したいDom要素の「ref属性」に「refオブジェクト」を渡します
「refオブジェクト」の「currentプロパティ」に値が保持されます

import { useRef } from 'react';
const App = () => {
  const el = useRef(null);
  const handleClick = () => {
    //Dom(input)のフォーカスメソッドを使う
    el.current.focus();
    console.log( el.current); //<input type="text">
  };
  return (
    <>
      <input ref={el} type="text" />
      <button onClick={handleClick}>フォーカスする</button>
    </>
  );
};

forwardRef

親コンポーネントから子コンポーネントのDOMを操作する場合
*注意:コンポーネント間に依存関係が強くなり、実質的に子から親にデータが流れます

子コンポーネントにref属性を追加するには
「forwardRef」を用いれば、関数コンポーネントで「ref 」が使えます
余談「ref」は親から子コンポーネントへ「props」で渡して参照するということができませんが「ref以外の名前」をつければ、「forwardRef」を使うことなく「props」として渡すことができます

  1. 関数コンポーネントをforwardRef()で囲みます
  2. 第一引数にprops、第二引数にrefを指定します
  3. 参照したいDOMのref属性にrefをセットします
import { useRef, forwardRef } from "react";

//子コンポーネント内でfowardRefを使用
const Input = forwardRef((props, ref) => {
  return <input type="text" ref={ref} />;
});

//親コンポーネントで「useRef」を使って子コンポーネントのDOMにアクセスします
const Example = () => {
  const ref = useRef();
  return (
    <>
      <Input ref={ref} />
      <button onClick={() => ref.current.focus()}>
      フォーカスする
      </button>
    </>
  );
};

export default Example;

createPortal

createPortalはコンポーネントの階層の外側に存在しているDOMノードに対してマウントできます
*モーダルやトーストのようなUIパーツの実装

//第一引数はReactの子要素としてレンダー可能なもの(要素、文字列、フラグメントなど)
//第二引数はマウント先のDom
createPortal(child, container)
import { createPortal } from "react-dom";

const Portal = ({ children }) => {
  const target = document.querySelector("マウント先のDom");
  return createPortal(children, target);
};

const App = () => {
  return (      
        <Portal>
         
        </Portal>
      )}
  );
};

export default App;

Redux Toolkit

「Redux Toolkit」はReactで「Redux」をより簡潔に使用できるツールです

Redux Toolkitを使うメリット

  • 「ActionCreators」は自動で作成されます(コード量が少なくなる)
  • stateのイミュータブル性を意識しなくてよい(Immerというライブラリを使用しているためです)
    *stateがオブジェクトでも新しいオブジェクトにする必要がない

*イミュータブル性について
値を更新する際は、必ず新しいオブジェクトを生成して返す必要があります(参照元が同じだと変化がないとみなされます)
例:state.item = “hoge”ではなく、{…state,item: “hoge”}

Reduxについて

グローバルなstate(アプリ全体で共有するstate)は「useContext」または「Redux(stateを管理をするためのフレームワーク)」を使用します
*同じ状態を共有して使用する複数のコンポーネントがある場合

全体の流れの要約

  • アプリの状態は、「ストアstore」と呼ばれるオブジェクトに存在します
  • stateの変更はaction(何が起こったかを説明するオブジェクト) を作成し、それをstore.dispatch()で渡します
    ActionCreators」はactionオブジェクトを作成する関数です
    コンポーネントで「ActionCreators」を実行するためにはdispatchが必要です
  • Reducer:(state, action) => newState
    state(現在のオブジェクト)とactionを受け取り、必要に応じて状態を更新する方法を決定して、新しい状態を返す関数です(以前の状態とactionを組み合わせて新しい状態を作ります)
    受け取ったアクションの種類に基づいてイベントを処理するイベントリスナー

Reducerの規則

  • stateおよびaction引数に基づいて新しい状態値のみを計算する必要があります
  • stateはコピーし、コピーした値に変更を加えます
  • 副作用となる処理(非同期を実行したり、ランダムな値を計算したり)は書けません

actionについて
storeに対して何かしたいときはactionを作成します(actionはオブジェクトで、typeプロパティが必要です)
typeプロパティは状態の更新のトリガーになります
action.payloadでreducerに値を渡すことができます(何が起こったかに関する追加情報)

{
  type: "アクションの種類を識別するための文字列またはシンボル",
  payload: "アクションの実行に必要な任意のデータ",
}

オブジェクトを手作業で記述するのはエラーの原因にもなので「ActionCreators」を使います
*actionオブジェクトを自分で記述する代わりに、オブジェクトを返す関数を呼び出します

function userLoggedIn() {
  return {
    type: 'USER_LOGGED_IN',
    username: 'dave'
  };
}

「Redux」でストアの作成(「Redux Toolkit」を使わない場合の)

import { createStore } from 'redux'

function counterReducer(state = { value: 0 }, action) {
  switch (action.type) {
    case 'counter/incremented':
      return { value: state.value + 1 }
    case 'counter/decremented':
      return { value: state.value - 1 }
    default:
      return state
  }
}
let store = createStore(counterReducer)

ストアから特定の情報を抽出
Selectors関数を定義しておくとアプリのさまざまな部分が同じデータを読み取る必要があるため、ロジックの繰り返しを避けるのに役立ちます
store.getState() : 現在の状態(state)

const selectCounterValue = state => state.value
const currentValue = selectCounterValue(store.getState())
console.log(currentValue)

アプリで何か発生した場合

  1. UIがactionをdispatchします
  2. ストアはreducerを実行し、何かに応じて状態が更新されます
  3. ストアは、状態の更新をUIに通知します
  4. UIは新しい状態に基づいて再レンダリングされます

Redux Toolkitを使ってみる

//create-react-appと同時に
npx create-react-app my-app --template redux
//Redux Toolkit と React-Redux パッケージをプロジェクトに追加
npm install @reduxjs/toolkit react-redux

configureStore(ストアの作成)
src/app/store.jsに空のストアを作成してエクスポートします

import { configureStore } from '@reduxjs/toolkit'
export const store = configureStore({
  reducer: {},
})

Provider
「index.js」に「store.jsファイル」をインポートし <Provider>のpropとしてストアを渡します

import React from 'react'
import ReactDOM from 'react-dom'
import './index.css'
import App from './App'
import { store } from './app/store'
import { Provider } from 'react-redux'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)

createSlice(Sliceファイルを作成します)
*Sliceファイルには「初期値・reducer(ActionCreator)」を設定します
counterSlice.jsファイルを作成します

import { createSlice } from '@reduxjs/toolkit'

const initialState = {
  value: 0,
}
export const counterSlice = createSlice({
  name: 'counter',
  initialState,
//メソッド毎に定義します(自動で同名の「ActionCreator」が作成されます)
  reducers: {
    increment: (state) => {
      state.value += 1
    },
    decrement: (state) => {
      state.value -= 1
    },
  // payloadを受け取る場合
    incrementByAmount: (state, action) => {
      state.value += action.payload
    },
  },
})

// ActionCreatorはコンポーネントでインポートします
export const { increment, decrement, incrementByAmount } = counterSlice.actions
//stateの値をコンポーネントでインポートします
export const selectCount = (state) => state.counter.value;
//store.jsでインポートしてreducerに登録します
export default counterSlice.reducer

注意(Immer利用の注意点):
Immerは既存の状態を変更(stateはオブジェクトまたは配列である必要があります)するか、新しい配列を返すことを期待していますが、同じ関数で両方を行うことはできません
*プリミティブは変更できないため、できることは新しい値を返すことだけです

import { createSlice, current } from '@reduxjs/toolkit';
reducers: {
//変更
    todoAdded(state, action) {
      state.push(action.payload)
    },
//新しい配列
    todoDeleted(state, action.payload) {
      return state.filter(todo => todo.id !== action.payload)
    }
  }
//新しい配列を保存して
  todoDeleted(state, action.payload) {
   // immutably
   const newTodos = state.todos.filter(todo => todo.id !== action.payload)
   // 変更
   state.todos = newTodos
}

//アロー関数で暗黙のreturnでこのルールが破られることに注意
  // NG: 新しい配列を返します
  brokenReducer: (state, action) => state.push(action.payload),
  //OK
  fixedReducer: (state, action) => {
      state.push(action.payload)
  },

//既存state全体を置き換えたい場合
 reducers: {
    brokenTodosLoadedReducer(state, action) {
       // NG (更新)
      state = action.payload
    },
    fixedTodosLoadedReducer(state, action) {
      // OK:古い値を置き換える新しい値を返します
      return action.payload
    },
    correctResetTodosReducer(state, action) {
      // OK
      return initialState
    },
  },
//ログ出力時はcurrent
reducers: {
    todoToggled(state, action) {
      // NG
      console.log(state)
      // OK
      console.log(current(state))
    },
  },

store.jsの編集
reducerをインポートしてストアに追加します

import { configureStore } from '@reduxjs/toolkit'
import counterReducer from './counterSlice'

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
})

ストアの初期状態
*「counter」は「createSliceのnameフィールド」の値です

{
  counter: {
    value: 0
  }
}

Counter.jsファイル
コンポーネントがストアと通信するには「useSelectoruseDispatch」を使います
useSelectorはコンポーネントでストアの状態から必要なデータを抽出できるフックです
*コンポーネントはストアと直接通信(store.getState())はできません
useDispatchはストアのメソッドだけにアクセスするためのフックです
*コンポーネントはストアに直接アクセス(store.dispatch(increment())はできません

import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux'
import { decrement, increment, incrementByAmount, selectCount } from './counterSlice'

const [incrementAmount, setIncrementAmount] = useState('2');
const incrementValue = Number(incrementAmount) || 0;

export function Counter() {
  const count = useSelector(selectCount)
  const dispatch = useDispatch()

  return (
    <div>
      <div>
        <button
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span>{count}</span>
        <button
          onClick={() => dispatch(decrement())}
        >
         -
        </button>
      </div>
      <div>
      <input
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
    {/* 入力値をactionのpayloadとして渡す */}
       <button
         onClick={() => dispatch(incrementByAmount(incrementValue))}
        >
         Add Amount
      </button>
      </div>
    </div>
  )
}

グローバルな状態はストアで管理、ローカルな状態(1つの場所でのみ必要な状態)はコンポーネントに保持します
*上の例では:カウンターに追加する数値の入力ボックス
const [incrementAmount, setIncrementAmount] = useState('2')

Redux Thunk

reducer関数は常に純粋関数でなければなりません
副作用となる処理(例えばサーバーとの通信など)は書けません
ミドルウェアを利用します

Redux Thunkはミドルウェアライブラリです(*Redux Toolkitではデフォルトで有効です)
複雑な同期ロジック・非同期の両方で使用できます
actionオブジェクトの代わりに関数を返すActionCreatorを書くことができます

Redux Thunkの基本の定義
*関数をreturnする関数を定義します(引数(payload)を渡します)
内側の関数はstoreのメソッドである「dispatch」と「getState(現在のstateの値を取得)」を引数として受け取ります

const thunkFunction = (payload) => { 
   return (dispatch, getState) => { 
     副作用となる処理
     ここで処理した変数をdispatchの引数(payload)として
     reducersのaction.payloadに渡すことができます
    } 
 }

ミドルウェアで条件分岐する

import { createSlice } from '@reduxjs/toolkit';

const initialState = {
  value: 0,
};
// ミドルウェアで条件分岐する(現在のstateが奇数のときだけdispatch)
export const incrementIfOdd = (payload) => (dispatch, getState) => {
  const currentValue = selectCount(getState());
  if (currentValue % 2 === 1) {
    dispatch(incrementByAmount(payload));
  }
};

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
  reducers: {
    increment: (state) => {
      state.value += 1;
    },
    decrement: (state) => {
      state.value -= 1;
    },
   incrementByAmount: (state, action) => {
      state.value += action.payload;
    },
  },
});

export const { increment, decrement, incrementByAmount } = counterSlice.actions;
export const selectCount = (state) => state.counter.value;
export default counterSlice.reducer;
import  { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {decrement, increment, incrementIfOdd, selectCount,} from './counterSlice';

export function Counter() {
  const count = useSelector(selectCount);
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');
  const incrementValue = Number(incrementAmount) || 0;

  return (
    <div>
      <div>
        <button
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
        <span>{count}</span>
        <button
          onClick={() => dispatch(increment())}
        >
          +
        </button>
      </div>
      <div >
        <input
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        <button
          onClick={() => dispatch(incrementIfOdd(incrementValue))}
        >
          Add If Odd
        </button>
      </div>
    </div>
  );
}

*サーバーとの通信時など、非同期処理のステータスも表示する
Toolkitのメソッド:createAsyncThunk(type文字列アクション値,コールバック,optionsオブジェクト)で必要なデータをreturnします
returnされたデータには「pending・fulfilled・rejected」が添付されます
戻り値は、createSliceの「extraReducersプロパティ」にセットします
*createAsyncThunkでreturnされたデータは「action.payload」に渡ります

import { createAsyncThunk, createSlice } from '@reduxjs/toolkit';

//2秒後にデータ取得される
function fetchCount(amount = 1) {
  return new Promise((resolve) =>
    setTimeout(() => resolve({ data: amount }), 2000)
  );
}

const initialState = {
  value: 0,
  status: 'idle',
};
//ミドルウェア
export const incrementAsync = createAsyncThunk(
  'counter/fetchCount',
  async (payload) => {
    const response = await fetchCount(payload);
    return response.data;
  }
);

export const counterSlice = createSlice({
  name: 'counter',
  initialState,
//extraReducers:生成したタイプ以外のアクションタイプに応答できます
  extraReducers: (builder) => {
    builder
      .addCase(incrementAsync.pending, (state) => {
        state.status = 'loading';
      })
      .addCase(incrementAsync.fulfilled, (state, action) => {
        state.status = 'idle';
        state.value += action.payload;
      });
  },
});

export const selectCount = (state) => state.counter.value;
export default counterSlice.reducer;
//コンポーネント側
import { useState } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import {incrementAsync, selectCount} from './counterSlice';

export function Counter() {
  const count = useSelector(selectCount);
  const statas = useSelector( (state) => state.counter.status )
  const dispatch = useDispatch();
  const [incrementAmount, setIncrementAmount] = useState('2');
  const incrementValue = Number(incrementAmount) || 0;

  return (
    <div>
        <p>{count}</p>
        <p>{statas}</p>
        <div>
        <input
          value={incrementAmount}
          onChange={(e) => setIncrementAmount(e.target.value)}
        />
        <button
          onClick={() => dispatch(incrementAsync(incrementValue))}
        >
          Add Async
        </button>
      </div>
    </div>
  );
}

備考:副作用について
下記のの要件(純粋関数の要件)を満たさない操作は副作用と呼ばれます

  • 関数の戻り値は引数のみに依存する
  • 外側のスコープを参照しない
  • 引数の値を変更しない
  • 関数外に影響を及ぼさない