Part 2: Redux App Structure

­YOON·2024년 11월 9일

Redux 핵심

목록 보기
2/3

The Counter Example App

npx create-react-app redux-essentials-example --template redux

Application Contents

  • /src
    - index.js : starting point for the app
    • App.js : the top-level React component
    • /app
      • store.js : creates the Redux store instance
    • /features
      • counter
        - Counter.js : a React component that shows the UI for the counter feature
        - counterSlice.js : the Redux logic for the counter feature

Creating the Redux Store

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

export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});
import { configureStore } from '@reduxjs/toolkit';
  • Redux store 설정
import counterReducer from '../features/counter/counterSlice';
  • counterReducer : reducer
  • Reducer
    - state와 action을 기반으로 새로운 상태를 반환하는 함수
    • counterSlice로부터 생성된 reducer를 가져옴
    • counterSlice는 createSlice를 사용하여 생성된 slice
export const store = configureStore({
  reducer: {
    counter: counterReducer,
  },
});
  • configureStore를 호출하여 Redux store 생성
  • reducer 객체는 앱의 상태를 관리하는 reducer 정의
    - counter라는 상태 slice를 counterReducer가 관리하도록 설정
    • 여러 reducer를 병합할 수 있는 구조
    • 각 reducer는 해당 slice의 상태를 관리함

Redux Slices

  • A collection of Redux reducer logic and actions for a single feature in your app
import { configureStore } from '@reduxjs/toolkit'
import usersReducer from '../features/users/usersSlice'
import postsReducer from '../features/posts/postsSlice'
import commentsReducer from '../features/comments/commentsSlice'

export default configureStore({
  reducer: {
    users: usersReducer,
    posts: postsReducer,
    comments: commentsReducer
  }
})
  • state.users, state.posts, and state.comments : seperate slice of the Redux state

Creating Slice Reducers and Actions

import { createSlice } from '@reduxjs/toolkit'

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payloaㅔd
    }
  }
})

export const { increment, decrement, incrementByAmount } = counterSlice.actions

export default counterSlice.reducer
import { createSlice } from '@reduxjs/toolkit'
  • createSlice : Reducer와 Action Creator 함께 정의할 수 있도록 허용함
  • Reducer와 행동 유형을 정의하지 않고 한 곳에서 상태 관리를 쉽게 접근할 수 있음
  • Takes care of the work of generating action type strings, action creator functions and action objects.
  • String from the name option is used as the first part of each action type
  • Key name of each reducer function is used as the second part
  • "counter" name + the "increment" reducer function generated an action type of { type: "counter/increment" }
console.log(counterSlice.actions.increment())
// {type: "counter/increment"}
const newState = counterSlice.reducer(
  { value: 10 },
  counterSlice.actions.increment()
)
console.log(newState)
// {value: 11}

Reducers and Immutable Updates

Our reducers are never allowed to mutate the original / current state values !

state.value = 123 // Illegal
return {
	...state,
    value: 123
}
// 기존 방식
function handwrittenReducer(state, action) {
  return {
    ...state,
    first: {
      ...state.first,
      second: {
        ...state.first.second,
        [action.someId]: {
          ...state.first.second[action.someId],
          fourth: action.someValue
        }
      }
    }
  }
}

// createSlice 이용 방식
function reducerWithImmer(state, action) {
  state.first.second[action.someId].fourth = action.someValue
}

You can only write "mutating" logic in Redux Toolkit's createSlice and createReducer because they use Immer inside! If you write mutating logic in reducers without Immer, it will mutate the state and cause bugs!

export const counterSlice = createSlice({
  name: 'counter',
  initialState: {
    value: 0
  },
  reducers: {
    increment: state => {
      // Redux Toolkit allows us to write "mutating" logic in reducers. It
      // doesn't actually mutate the state because it uses the immer library,
      // which detects changes to a "draft state" and produces a brand new
      // immutable state based off those changes
      state.value += 1
    },
    decrement: state => {
      state.value -= 1
    },
    incrementByAmount: (state, action) => {
      state.value += action.payload
    }
  }
})
  • Immer 라이브러리를 사용하여 불변성을 자동 처리
  • 직접적으로 상태를 변경하는 것처럼 작성할 수 있지만, 실제로는 불변 상태 유지

Writing Async Logic with Thunks

Thunk

  • Redux function that can contain asynchronous logic
  • Thunks are written using two functions
    • Inside thunk function : gets dispatch and getState as arguments
    • Outer creator function : creates and returns the thunk function
// The function below is called a thunk and allows us to perform async logic.
// It can be dispatched like a regular action: `dispatch(incrementAsync(10))`.
// This will call the thunk with the `dispatch` function as the first argument.
// Async code can then be executed and other actions can be dispatched
export const incrementAsync = amount => dispatch => {
  setTimeout(() => {
    dispatch(incrementByAmount(amount))
  }, 1000)
}

// We can use them the same way we use a typical Redux action creator
store.dispatch(incrementAsync(5))
// the outside "thunk creator" function
const fetchUserById = userId => {
  // the inside "thunk function"
  return async (dispatch, getState) => {
    try {
      // make an async call in the thunk
      const user = await userAPI.fetchById(userId)
      // dispatch an action when we get the response back
      dispatch(userLoaded(user))
    } catch (err) {
      // If something went wrong, handle it here
    }
  }
}
  • 서버에 데이터를 가져오면서 AJAX 호출을 해야 할 떄, 그 호출을 thunk에 넣을 수 있음

React Counter

import React, { useState } from 'react'
import { useSelector, useDispatch } from 'react-redux'
import {
  decrement,
  increment,
  incrementByAmount,
  incrementAsync,
  selectCount
} from './counterSlice'
import styles from './Counter.module.css'

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

  return (
    <div>
      <div className={styles.row}>
        <button
          className={styles.button}
          aria-label="Increment value"
          onClick={() => dispatch(increment())}
        >
          +
        </button>
        <span className={styles.value}>{count}</span>
        <button
          className={styles.button}
          aria-label="Decrement value"
          onClick={() => dispatch(decrement())}
        >
          -
        </button>
      </div>
      {/* omit additional rendering output here */}
    </div>
  )
}
  • const count = useSelector(selectCount)

  • React includes several built-in hooks like useState and `useEffect

  • Other libraries can create their own custom hooks that use React's hooks to build custom logic

Reading Data with useSelector

  • Lets our component extract whatever pieces of data it needs from the Redux store state
  • "selector" function => take state as an argument and return some parts of the state value
// The function below is called a selector and allows us to select a value from
// the state. Selectors can also be defined inline where they're used instead of
// in the slice file. For example: `useSelector((state) => state.counter.value)`
export const selectCount = state => state.counter.value

const count = selectCount(store.getState())
console.log(count)
// 0
  • Our components can't talk to the Redux store directly, because we're not allowed to import it into component files.
  • But useSelector takes care of talking to the Redux store behinds the scenes for us.
  • If pass in a selector function, it calls someSelector(store.getState()) for us and returns the result
  • So we can get the current store counter value by doing:
const count = useSelector(selectCount)
const countPlusTwo = useSelector(state => state.counter.value + 2)

Dispatching Actions with useDispatch

const dispatch = useDispatch()
<button
  className={styles.button}
  aria-label="Increment value"
  onClick={() => dispatch(increment())}
>
  +
</button>

Component State and Forms

Do I always have to put all my app's state into the Redux store? NO.

  • Global state that is neeeded across the app should go in the Redux store.
  • State that's only needed in one place should be kept in component state.
const [incrementAmount, setIncrementAmount] = useState('2')

// later
return (
  <div className={styles.row}>
    <input
      className={styles.textbox}
      aria-label="Set increment amount"
      value={incrementAmount}
      onChange={e => setIncrementAmount(e.target.value)}
    />
    <button
      className={styles.button}
      onClick={() => dispatch(incrementByAmount(Number(incrementAmount) || 0))}
    >
      Add Amount
    </button>
    <button
      className={styles.asyncButton}
      onClick={() => dispatch(incrementAsync(Number(incrementAmount) || 0))}
    >
      Add Async
    </button>
  </div>
)
  • Keep the current number string in the Redux store by dispatching an action in the input's onChange handler and keep it in our reducer

  • dispatch(incrementAsync(Number(incrementAmount) || 0))}

  • React+Redux App
    - Global State -> Redux store

    • Local State -> React Component

Providing the Store

  • Components use the useSelector and useDispatch hooks to talk to the Redux store
// index.js
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'
import * as serviceWorker from './serviceWorker'

ReactDOM.render(
  <Provider store={store}>
    <App />
  </Provider>,
  document.getElementById('root')
)
  • ReactDOM.render(<App />) - Tell React to start rendering our root ` Component
  • <Provider>
    - Hooks(useSelector) to work, use component to pass down the Redux store

0개의 댓글