React Redux Toolkit Posts 연습하기 ( 이모션 추가 )
이름을 선택하고 제목과 내용을 입력하고 저장을하면 Redux를 통해 아래처럼 저장되는 형태입니다.
id는 redux toolkit의 nanoid를 사용했습니다. date는 date-fns library를 사용했습니다. ( date-fns 바로가기 )
./app/store.ts
// 툴킷에서 구성 저장소를 가져옵니다.
import { configureStore } from "@reduxjs/toolkit";
// Slice
import postsReducer from "../features/posts/postsSlice";
import usersReducer from "../features/users/usersSlice";
export const store = configureStore({
reducer: {
posts: postsReducer,
users: usersReducer
}
});
store.ts파일을 생성하고, Redux Toolkit에서 configureStore API를 불러옵니다.
reducer 매개변수 내부에 필드를 정의해서 스토어에 리듀서 기능을 사용하여 해당 상태에 대한 모든 업데이트를 처리하도록 해줍니다. ( 여러개를 지정할 수 있습니다. )
./features/posts/postsSlice.ts
import { createSlice, nanoid, PayloadAction } from "@reduxjs/toolkit";
import { sub } from "date-fns";
import { PostsListProps, PostsProps, ReactionProps } from "../../api/interface";
const initialState = [
{
id: "1",
title: "제목입니다.",
content: "Hello World 내용입니다~",
date: sub(new Date(), { minutes: 10 }).toISOString(),
reactions: {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0
}
},
{
id: "2",
title: "Slice TEST !",
content: "Slice TEST ing~~",
date: sub(new Date(), { minutes: 5 }).toISOString(),
reactions: {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0
}
},
];
const postsSlice = createSlice({
name: "posts",
initialState,
reducers: {
postAdded: {
reducer(state, action: PayloadAction<PostsProps>) {
state.push(action.payload);
},
prepare(title: string, content: string, userId: string) {
return {
payload: {
id: nanoid(),
title,
content,
userId,
date: new Date().toISOString(),
reactions: {
thumbsUp: 0,
wow: 0,
heart: 0,
rocket: 0,
coffee: 0
}
}
}
}
},
reactionAdded(state, action: PayloadAction<ReactionProps>) {
const { postId, reaction } = action.payload;
const existingPost = state.find(post => post.id === postId);
if (existingPost) {
switch(reaction) {
case "thumbsUp" : existingPost.reactions[reaction]++; break;
case "wow" : existingPost.reactions[reaction]++; break;
case "heart" : existingPost.reactions[reaction]++; break;
case "rocket" : existingPost.reactions[reaction]++; break;
case "coffee" : existingPost.reactions[reaction]++; break;
default : break;
}
// 방법 찾기
// existingPost.reactions[reaction]++
}
}
}
});
export const selectAllPosts = (state: PostsListProps) => state.posts;
export const { postAdded, reactionAdded } = postsSlice.actions;
export default postsSlice.reducer;
postsSlice.ts파일을 생성하고 createSlice API를 불러옵니다.
슬라이스를 만들기 위해서는 슬라이스를 식별하는 문자열 이름, 초기 상태 값, 상태를 업데이트하는 방법을 정의하는 하나 이상의 축소기 함수가 필요합니다. 슬라이스가 생성되면 생성된 Redux 액션 생성자와 전체 슬라이스에 대한 리듀서 함수를 내보낼 수 있습니다.
연습하기 ( 2 )와는 다르게 reducers내부에 reducer와 prepare를 추가해 주었습니다.
- reducer: reducer의 기본 state값을 지정해줍니다. reducer에서 state가 최초의 initialState입니다. dispatch가 작동할 때마다 action을 통해 reducer가 동작해서 state를 바꾸는 식으로 redux가 작동합니다.
- prepare: prepare는 createAction 함수의 두번째 파라미터인 콜백 함수에 해당합니다.
./features/users/usersSlice.ts
import { createSlice } from "@reduxjs/toolkit";
import { UsersProps } from "../../api/interface";
const initialState = [
{
id: "0",
name: "Jun"
},
{
id: "1",
name: "Shiro"
},
{
id: "2",
name: "Run"
}
]
const usersSlice = createSlice({
name: "users",
initialState,
reducers: {}
});
export const selectAllUsers = (state: UsersProps) => state.users;
export default usersSlice.reducer;
./index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import App from './App';
// redux
import { store } from './app/store';
import { Provider } from 'react-redux';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<Provider store={store}>
<App />
</Provider>
);
index.tsx에 Provider를 배치해서 React 구성 요소를 사용할 수 있도록 해줍니다.
위 내용은 생성해준 store를 가져와서 App 주위에 Provider를 배치하고 해당 스토어를 소품으로 전달해줍니다.
./App.tsx
import { BrowserRouter as Router, Route, Routes } from 'react-router-dom';
import { PostsList } from './features/posts/PostsList';
function App() {
return (
<Router>
<Routes>
<Route path="/posts-list" element={<PostsList />} />
</Routes>
</Router>
);
}
export default App;
./features/posts/PostsList.tsx
import styles from "./PostsList.module.scss";
import { useSelector } from "react-redux";
import { selectAllPosts } from "./postsSlice";
import { AddPostForm } from "./AddPostForm";
import { PostAuth } from "./PostAuth";
import { Time } from "./Time";
import { ReactionButton } from "./ReactionButton";
export const PostsList = () => {
const posts = useSelector(selectAllPosts);
const orderedPosts = posts.slice().sort((a, b) => b.date.localeCompare(a.date));
const renderedPosts = orderedPosts.map(post => (
<section key={post.id} className={styles.posts_wrap}>
<h3>{post.title}</h3>
<p>{post.content.substring(0, 100)}</p>
<p>
<PostAuth userId={post.userId} />
<Time timestamp={post.date} />
</p>
<ReactionButton post={post} />
</section>
));
return (
<article className={styles.posts_list_wrap}>
<h2>Posts</h2>
<AddPostForm />
<div className={styles.posts_list}>
{renderedPosts}
</div>
</article>
);
};
./features/posts/PostAuth.tsx
import { useSelector } from "react-redux";
import { selectAllUsers } from "../users/usersSlice";
export const PostAuth = ({ userId }: { userId: string }) => {
const users = useSelector(selectAllUsers);
const auth = users.find(user => user.name === userId);
return (
<span>By { auth ? auth.name : "Unknown Auth" }</span>
);
};
./features/posts/Time.tsx
import { parseISO, formatDistanceToNow } from "date-fns";
export const Time = ({ timestamp }: { timestamp: string }) => {
let newTime = "";
if (timestamp) {
const date = parseISO(timestamp);
const timePeriod = formatDistanceToNow(date);
newTime = `${timePeriod} ago`;
}
return (
<span title={timestamp}>
<i>{newTime}</i>
</span>
);
}
./features/posts/PostAuth.tsx
import { useSelector } from "react-redux";
import { selectAllUsers } from "../users/usersSlice";
export const PostAuth = ({ userId }: { userId: string }) => {
const users = useSelector(selectAllUsers);
const auth = users.find(user => user.name === userId);
return (
<span>By { auth ? auth.name : "Unknown Auth" }</span>
);
};
./features/posts/ReactionButton.tsx
// Control + Command + Space = 👍, 😮, 🧡, 🚀, 🧋
import { useDispatch } from "react-redux";
import { PostsProps } from "../../api/interface";
import { reactionAdded } from "./postsSlice";
const reactionEmoji = {
thumbsUp: "👍",
wow: "😮",
heart: "🧡",
rocket: "🚀",
coffee: "🧋"
}
export const ReactionButton = ({ post }: { post: PostsProps }) => {
console.log("POST", post)
const dispatch = useDispatch();
const reactionButton = Object.entries(reactionEmoji).map(([name, emoji], index) => {
return (
<button key={name} type="button" onClick={() => dispatch(reactionAdded({ postId: post.id, reaction: name}))}>
{emoji}
{Object.values(post.reactions)[index]}
</button>
);
})
return (
<div>{reactionButton}</div>
);
}
./api/interface.ts
// Posts 관련
export interface PostsListProps {
posts: [
{
id: string,
title: string,
content: string,
userId: string,
date: string,
reactions: {
thumbsUp: number,
wow: number,
heart: number,
rocket: number,
coffee: number
}
}
]
}
export interface PostsProps {
id: string,
title: string,
content: string,
userId: string,
date: string,
reactions: {
thumbsUp: number,
wow: number,
heart: number,
rocket: number,
coffee: number
}
}
export interface ReactionProps {
postId: string,
reaction: string
}
// Users 관련
export interface UsersProps {
users: [
{
id: string,
name: string
}
]
}
'React > React' 카테고리의 다른 글
[ React ] Warning: Assign arrow function to a variable before exporting as module default import/no-anonymous-default-export (0) | 2023.03.14 |
---|---|
[ React ] 객체 배열 State값 변경하기 (0) | 2023.03.08 |
[ React ] Redux Toolkit 연습하기 ( 2 ) (0) | 2023.01.26 |
[ React ] Redux Toolkit 연습하기 ( 1 ) (0) | 2023.01.26 |
[ React ] React CRUD ( Feat. Typescript ) (0) | 2023.01.11 |