[react]第3週開発ログ
この文章はスパティンコードクラブ「フロントの花、反応」の3週間の講座を聞いて書いたものだ.
:サーバにhtmlが1つしかないアプリケーション
:ブラウザのアドレスによって異なるページが表示されます.
インストール:糸add react-router-dom index.jsに追加された App.js を正確に追加すると、ホームコンポーネントの重複データ を排除できます.履歴オブジェクトを支柱として受信するには、withRouter設定 を使用します.
:データを一つの場所に集めて、あちこちから出してみます.
インストール:糸add redux react-redux
1)状態:格納された状態値(=データ)
2)動作:状態変化が必要な場合に発生する動作
3)ActionCreator:アクション作成関数.アクションの作成
4)Reducer:リードに格納されている状態を変更する関数
5)Store:reduceを適用するために作成
6)dispatch:アニメーションを刺激するキャラクター Ridexフィーチャー
1)storeは1個のみ使用
2)storeの状態(データ)はactionにしか変更できません
3)Reduserは、どのような要求があっても、同じ動作を実行する必要がある .ステータス管理フロー
Ridex Storeをコンポーネントに接続します.
コンポーネントが状態変化を必要とする場合にActionが呼び出されます.
Reducerで新しい状態値を作成します.
新しい状態値をStoreに保存します.
コンポーネントは新しいステータス値を受け入れます.
ガイドを使用して遺願リストデータを削除
./redux/modules/bucket.js
./redux/configStore.js App.js Detail.js BucketList.js Quiz.js 結果画面
-希望リスト項目を追加した後、詳細ページ
の詳細ページで「削除」をクリックし、戻りながらリストから削除します.
App.js Start.js Score.js Ranking.js Message.js ./redux/modules/quiz.js ./redux/modules/rank.js 結果画面
❗詳細スタイルはまだきれいに適用されていません.もっとスタイルを重視して完成する必要があります.
「100 Quiz」画面でswifeを1回行う場合は、2回発生したエラーを修正する必要があります.
SPA(Single Page Application)
:サーバにhtmlが1つしかないアプリケーション
ルート
:ブラウザのアドレスによって異なるページが表示されます.
インストール:糸add react-router-dom
import { BrowserRouter } from "react-router-dom";
ReactDOM.render(
<BrowserRouter>
<App />
</BrowserRouter>,
document.getElementById("root")
);
import { Route } from "react-router-dom";
import { withRouter } from "react-router";
...
return (
<div className="App">
<div>
// 링크 연결
<Link to="/">Home으로 가기</Link>
<Link to="/cat">Cat으로 가기</Link>
</div>
// 아래와 같이 Route 사용
<Route path="/" exact component={Home} />
<Route path="/cat" component={Cat} />
<button onClick={() => {
//push() : 페이지 이동
this.props.history.push('/cat');
}}>
/cat으로 가기
</button>
<button onClick={()=>{
// goBack(): 뒤로가기
this.props.history.goBack();
}}>뒤로가기
</button>
</div>
);
}
}
export default withRouter(App);
じょうちょう
:データを一つの場所に集めて、あちこちから出してみます.
インストール:糸add redux react-redux
1)状態:格納された状態値(=データ)
2)動作:状態変化が必要な場合に発生する動作
3)ActionCreator:アクション作成関数.アクションの作成
4)Reducer:リードに格納されている状態を変更する関数
5)Store:reduceを適用するために作成
6)dispatch:アニメーションを刺激するキャラクター
1)storeは1個のみ使用
2)storeの状態(データ)はactionにしか変更できません
3)Reduserは、どのような要求があっても、同じ動作を実行する必要がある
Ridex Storeをコンポーネントに接続します.
コンポーネントが状態変化を必要とする場合にActionが呼び出されます.
Reducerで新しい状態値を作成します.
新しい状態値をStoreに保存します.
コンポーネントは新しいステータス値を受け入れます.
[Toy Project] BucketList
ガイドを使用して遺願リストデータを削除
![](https://s1.md5.ltd/image/72d51c9285474c9c237c4682a03cccfa.png)
./redux/modules/bucket.js
// Actions
const LOAD = "bucket/LOAD";
const CREATE = "bucket/CREATE";
const DELETE = "bucket/DELETE";
const initialState = {
list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
};
// Action Creators
export const loadBucket = (bucket) => {
return { type: LOAD, bucket };
};
export const createBucket = (bucket) => {
// bucket = text
return { type: CREATE, bucket };
};
export const deleteBucket = (bucket) => {
return { type: DELETE, bucket };
};
// Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
// do reducer stuff
case "bucket/LOAD":
return state;
case "bucket/CREATE": {
const new_bucket_list = [...state.list, action.bucket];
return { list: new_bucket_list };
}
case "bucket/DELETE": {
const bucket_list = state.list.filter((l, idx) => {
if (idx !== action.bucket) {
return l;
}
});
return { list: bucket_list };
}
default:
return state;
}
}
import { createStore, combineReducers } from "redux";
import bucket from "./modules/bucket";
import { createBrowserHistory } from "history";
export const history = createBrowserHistory();
const rootReducer = combineReducers({ bucket });
const store = createStore(rootReducer);
export default store;
import React from "react";
import styled from "styled-components";
import { withRouter } from "react-router";
import { Route, Switch } from "react-router-dom";
import BucketList from "./BucketList";
import Detail from "./Detail";
import NotFound from "./NotFound";
import { connect } from "react-redux";
import { loadBucket, createBucket } from "./redux/modules/bucket";
const mapStateToProps = (state) => {
return { bucket_list: state.bucket.list };
};
const mapDispatchToProps = (dispatch) => {
return {
load: () => {
dispatch(loadBucket());
},
create: (bucket) => {
dispatch(createBucket(bucket));
},
};
};
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
list: ["영화관 가기", "매일 책읽기", "수영 배우기"],
};
this.text = React.createRef();
}
componentDidMount() {
console.log(this.text);
}
addBucketList = () => {
const new_item = this.text.current.value;
this.props.create(new_item);
};
render() {
return (
<div className="App">
<Container>
<Title>내 버킷리스트</Title>
<Line />
<Switch>
<Route
path="/"
exact
render={(props) => (
<BucketList
bucket_list={this.props.bucket_list}
history={this.props.history}
/>
)}
/>
<Route path="/detail/:index" component={Detail} />
<Route
render={(props) => <NotFound history={this.props.history} />}
/>
</Switch>
</Container>
<Input>
<input type="text" ref={this.text} />
<button onClick={this.addBucketList}>추가하기</button>
</Input>
</div>
);
}
}
const Input = styled.div`
max-width: 350px;
min-height: 10vh;
background-color: #fff;
padding: 16px;
margin: 20px auto;
border-radius: 5px;
border: 1px solid #ddd;
`;
const Container = styled.div`
max-width: 350px;
min-height: 80vh;
background-color: #fff;
padding: 16px;
margin: 20px auto;
border-radius: 5px;
border: 1px solid #ddd;
`;
const Title = styled.h1`
color: slateblue;
text-align: center;
`;
const Line = styled.hr`
margin: 16px 0px;
border: 1px dotted #ddd;
`;
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
import React from "react";
import { useSelector, useDispatch } from "react-redux";
import { deleteBucket } from "./redux/modules/bucket";
const Detail = (props) => {
const dispatch = useDispatch();
const bucket_list = useSelector((state) => state.bucket.list);
console.log(bucket_list, props);
const bucket_index = parseInt(props.match.params.index);
return (
<div>
<h1>{bucket_list[bucket_index]}</h1>
<button
onClick={() => {
dispatch(deleteBucket(bucket_index));
props.history.goBack();
}}
>
삭제하기
</button>
</div>
);
};
export default Detail;
import React from "react";
import styled from "styled-components";
import { useDispatch, useSelector } from "react-redux";
const BucketList = (props) => {
const bucket_list = useSelector((state) => state.bucket.list);
console.log(bucket_list);
return (
<ListStyle>
{bucket_list.map((list, index) => {
return (
<ItemStyle
className="list_item"
key={index}
onClick={() => {
props.history.push("/detail/" + index);
}}
>
{list}
</ItemStyle>
);
})}
</ListStyle>
);
};
const ListStyle = styled.div`
display: flex;
flex-direction: column;
height: 100%;
overflow-x: hidden;
overflow-y: auto;
`;
const ItemStyle = styled.div`
padding: 16px;
margin: 8px;
background-color: aliceblue;
`;
export default BucketList;
import React from "react";
import styled from "styled-components";
import img from "./ponyo.jpg";
import TinderCard from "react-tinder-card";
import SwipeItem from "./SwiptItem";
import Score from "./Score";
import { useSelector, useDispatch } from "react-redux";
import { addAnswer } from "./redux/modules/quiz";
const Quiz = (props) => {
const dispatch = useDispatch();
const answers = useSelector((state) => state.quiz.answers);
const quiz = useSelector((state) => state.quiz.quiz);
const num = answers.length;
const onSwipe = (direction) => {
let _answer = direction === "left" ? "O" : "X";
if (_answer === quiz[num].answer) {
// 정답일 경우,
dispatch(addAnswer(true));
} else {
// 오답일 경우,
dispatch(addAnswer(false));
}
};
if (num > quiz.length - 1) {
return <Score {...props} />;
// return <div>퀴즈 끝!</div>;
}
return (
<QuizContainer>
<p>
<span>{num + 1}번 문제</span>
</p>
{quiz.map((l, idx) => {
if (num === idx) {
return <Question key={idx}>{l.question}</Question>;
}
})}
<AnswerZone>
<Answer>O</Answer>
<Answer>X</Answer>
</AnswerZone>
{quiz.map((l, idx) => {
if (idx === num) {
return (
<DragItem key={idx}>
<TinderCard
onSwipe={onSwipe}
onCardLeftScreen={onSwipe}
onCardRightScreen={onSwipe}
preventSwipe={["up", "down"]}
>
<img src={img} />
</TinderCard>
</DragItem>
);
// <SwipeItem key={idx} onSwipe={onSwipe} />;
}
})}
</QuizContainer>
);
};
const QuizContainer = styled.div`
text-align: center;
margin-top: 130px;
& > p > span {
padding: 8px 16px;
background-color: #ffe08c;
border-radius: 30px;
font-weight: bold;
}
`;
const Question = styled.h1`
font-size: 1.5em;
`;
const AnswerZone = styled.div`
width: 100vw;
height: 100vh;
display: flex;
position: absolute;
top: 0;
left: 0;
z-index: 1;
`;
const Answer = styled.div`
width: 50%;
display: flex;
justify-content: center;
align-items: center;
font-size: 100px;
font-weight: 600;
color: #dadafc77;
`;
const DragItem = styled.div`
display: flex;
align-items: center;
justify-content: center;
position: fixed;
top: 0;
left: 0;
width: 100vw;
height: 100vh;
z-index: 10;
& img {
max-width: 130px;
}
`;
const Finish = styled.p`
text-align: center;
margin-top: 300px;
font-weight: 600;
font-size: 30px;
`;
export default Quiz;
-希望リスト項目を追加した後、詳細ページ
![](https://s1.md5.ltd/image/ec68e18bf35a92b8b9ad1e38d033bf3d.png)
![](https://s1.md5.ltd/image/bc01426ba996185f90a49789829328d0.jpg)
[HW] FriendTest Project
![](https://s1.md5.ltd/image/1f3926c526b04d5935ba5333ab92fc69.png)
import React from "react";
import "./App.css";
import { Route, Switch } from "react-router-dom";
import Start from "./Start";
import Quiz from "./Quiz";
import Score from "./Score";
import Message from "./Message";
import Ranking from "./Ranking";
import { withRouter } from "react-router";
import { connect } from "react-redux";
const mapStateToProps = (state) => ({
...state,
});
const mapDispatchToProps = (dispatch) => ({
load: () => {},
});
class App extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
return (
<div className="App">
<Switch>
<Route path="/" exact component={Start} />
<Route path="/quiz" component={Quiz} />
<Route path="/score" component={Score} />
<Route path="/message" component={Message} />
<Route path="/ranking" component={Ranking} />
</Switch>
</div>
);
}
}
export default connect(mapStateToProps, mapDispatchToProps)(withRouter(App));
import React from "react";
import img from "./ponyo.jpg";
import { useDispatch, useSelector } from "react-redux";
import { addUserName } from "./redux/modules/rank";
const Start = (props) => {
const dispatch = useDispatch();
const name = useSelector((state) => state.quiz.name);
const input_text = React.useRef(null);
return (
<div className="container">
<div className="outter">
<img className="scc-img" src={img} />
<h1>
나는 <span>{name}</span>에 대해 얼마나 알고 있을까?
</h1>
<input
ref={input_text}
className="text-box"
type="text"
placeholder="내 이름"
/>
<button
className="button"
onClick={() => {
dispatch(addUserName(input_text.current.value));
props.history.push("/quiz");
}}
>
시작하기
</button>
</div>
</div>
);
};
export default Start;
import React from "react";
import styled from "styled-components";
import { useSelector, useDispatch } from "react-redux";
import { addRank } from "./redux/modules/rank";
const Score = (props) => {
const name = useSelector((state) => state.quiz.name);
const score_texts = useSelector((state) => state.quiz.score_texts);
const answers = useSelector((state) => state.quiz.answers);
let correct = answers.filter((answer) => {
return answer;
});
let score = (correct.length / answers.length) * 100;
let score_text = "";
Object.keys(score_texts).map((s, idx) => {
if (idx === 0) {
score_text = score_texts[s];
}
score_text = parseInt(s) <= score ? score_texts[s] : score_text;
});
return (
<ScoreContainer>
<Text>
<span>{name}</span>
퀴즈에 <br />
대한 내 점수는?
</Text>
<MyScore>
<span>{score}</span>점<p>{score_text}</p>
</MyScore>
<Button
onClick={() => {
props.history.push("/message");
}}
outlined
>
{name}에게 한마디
</Button>
</ScoreContainer>
);
};
const ScoreContainer = styled.div`
display: flex;
width: 100vw;
height: 100vh;
overflow: hidden;
padding: 20px;
box-sizing: border-box;
flex-direction: column; //세로로 객체 배열
justify-content: center;
align-items: center;
`;
const Text = styled.h1`
font-size: 1.5em;
margin: 0px;
line-height: 1.7;
text-align: center;
& span {
background-color: #ffe08c;
padding: 5px 10px;
border-radius: 30px;
}
`;
const MyScore = styled.div`
& span {
border-radius: 25px;
padding: 5px 10px;
background-color: #ffe08c;
}
font-weight: 600;
font-size: 2em;
margin: 25px;
text-align: center;
& > p {
margin: 20px 0px;
font-size: 18px;
font-weight: 550;
}
`;
const Button = styled.button`
color: white;
padding: 10px 20px;
background-color: #6799ff;
border-radius: 30px;
margin: 10px;
border: 1px solid #b2ccff;
width: 70vw;
`;
export default Score;
import React from "react";
import styled from "styled-components";
import { useSelector, useDispatch } from "react-redux";
import { resetAnswer } from "./redux/modules/quiz";
const Ranking = (props) => {
const dispatch = useDispatch();
const _ranking = useSelector((state) => state.rank.ranking);
const ranking = _ranking.sort((a, b) => {
return b.score - a.score;
});
return (
<RankContainer>
<Topbar>
<p>
<span>{ranking.length}명</span>의 사람들 중 당신은?
</p>
</Topbar>
<RankWrap>
{ranking.map((r, idx) => {
return (
<RankItem key={idx} highlight={r.current ? true : false}>
<RankNum>{idx + 1}등</RankNum>
<RankUser>
<p>
<b>{r.name}</b>
</p>
<p>{r.message}</p>
</RankUser>
</RankItem>
);
})}
</RankWrap>
<Button
onClick={() => {
dispatch(resetAnswer());
window.location.href = "/";
}}
>
다시 하기
</Button>
</RankContainer>
);
};
const RankContainer = styled.div`
width: 100%;
padding-bottom: 100px;
`;
const Topbar = styled.div`
position: fixed;
top: 0;
left: 0;
width: 100vw;
min-height: 50px;
border-bottom: 1px solid #ddd;
background-color: #fff;
& > p {
text-align: center;
}
& > p > span {
border-radius: 30px;
background-color: #fef5d4;
font-weight: 600;
padding: 4px 8px;
}
`;
const RankWrap = styled.div`
display: flex;
flex-direction: column;
width: 100%;
margin-top: 58px;
`;
const RankItem = styled.div`
width: 80vw;
margin: 8px auto;
display: flex;
border-radius: 5px;
border: 1px solid #ddd;
padding: 8px 16px;
align-items: center;
background-color: ${(props) => (props.highlight ? "#ffd6aa" : "#ffffff")};
`;
const RankNum = styled.div`
text-align: center;
font-size: 2em;
font-weight: 600;
padding: 0px 16px 0px 0px;
border-right: 1px solid #ddd;
`;
const RankUser = styled.div`
padding: 8px 16px;
text-align: left;
& > p {
&:first-child > b {
border-bottom: 2px solid #212121;
}
margin: 0px 0px 8px 0px;
}
`;
const Button = styled.button`
position: fixed;
bottom: 5vh;
left: 0;
padding: 8px 24px;
background-color: ${(props) => (props.outlined ? "#ffffff" : "#dadafc")};
border-radius: 30px;
margin: 0px 10vw;
border: 1px solid #dadafc;
width: 80vw;
`;
export default Ranking;
import React from "react";
import img from "./ponyo.jpg";
import { useDispatch, useSelector } from "react-redux";
import { addRank } from "./redux/modules/rank";
const Message = (props) => {
const dispatch = useDispatch();
const name = useSelector((state) => state.quiz.name);
const answers = useSelector((state) => state.quiz.answers);
const user_name = useSelector((state) => state.rank.user_name);
const input_text = React.useRef(null);
let correct = answers.filter((answer) => {
return answer;
});
let score = (correct.length / answers.length) * 100;
return (
<div
style={{
display: "flex",
height: "100vh",
width: "100vw",
overflow: "hidden",
padding: "16px",
boxSizing: "border-box",
}}
>
<div
className="outter"
style={{
display: "flex",
alignItems: "center",
justifyContent: "center",
flexDirection: "column",
height: "100vh",
width: "100vw",
overflow: "hidden",
padding: "0px 10vw",
boxSizing: "border-box",
maxWidth: "400px",
margin: "0px auto",
}}
>
<img
src={img}
style={{ width: "80%", margin: "-70px 16px 48px 16px" }}
/>
<h1 style={{ fontSize: "1.5em", margin: "0px", lineHeight: "1.4" }}>
<span
style={{
backgroundColor: "#fef5d4",
padding: "5px 10px",
borderRadius: "30px",
}}
>
{name}
</span>
에게 한마디
</h1>
<input
ref={input_text}
type="text"
style={{
padding: "10px",
margin: "24px 0px",
border: "1px solid #dadafc",
borderRadius: "30px",
width: "100%",
}}
placeholder="한 마디 적기"
/>
<button
onClick={() => {
let rank_info = {
score: parseInt(score),
name: user_name,
message: input_text.current.value,
current: true,
};
dispatch(addRank(rank_info));
props.history.push("/ranking");
}}
style={{
padding: "8px 24px",
backgroundColor: "#dadafc",
borderRadius: "30px",
border: "#dadafc",
}}
>
한마디하고 랭킹 보러 가기
</button>
</div>
</div>
);
};
export default Message;
//Actions
const GET_QUIZ = "quiz/GET_QUIZ";
const ADD_ANSWER = "quiz/ADD_ANSWER";
const RESET_ANSWER = "quiz/RESET_ANSWER";
const initialState = {
name: "포뇨",
score_texts: {
60: "우린 친구! 앞으로도 더 친하게 지내요!",
80: "우와! 우리는 엄청 가까운 사이!",
100: "우린 둘도 없는 단짝! :)",
},
answers: [],
quiz: [
{ question: "포뇨는 5살이다.", answer: "O" },
{ question: "포뇨는 주황색 머리다.", answer: "O" },
{ question: "포뇨는 물을 좋아한다.", answer: "O" },
{ question: "포뇨는 라면을 좋아한다.", answer: "O" },
],
};
export const getQuiz = (quiz_list) => {
return { type: GET_QUIZ, quiz_list };
};
export const addAnswer = (answer) => {
return { type: ADD_ANSWER, answer };
};
export const resetAnswer = () => {
return { type: RESET_ANSWER };
};
//Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case "quiz/GET_QUIZ": {
return { ...state, quiz: action.quiz_list };
}
case "quiz/ADD_ANSWER": {
return { ...state, answers: [...state.answers, action.answer] };
}
case "quiz/RESET_ANSWER": {
return { ...state, answers: [] };
}
default:
return state;
}
}
// Actions
const ADD_USER_NAME = "rank/ADD_USER_NAME";
const ADD_USER_MESSAGE = "rank/ADD_USER_MESSAGE";
const ADD_RANK = "rank/ADD_RANK";
const GET_RANK = "rank/GET_RANK";
const initialState = {
user_name: "",
user_message: "",
user_score: "",
score_text: {
60: "우린 친구! 앞으로도 더 친하게 지내요!",
80: "우와! 우리는 엄청 가까운 사이!",
100: "우린 둘도 없는 단짝! :)",
},
ranking: [{ score: 40, name: "최수빈", message: "안녕 포뇨!" }],
};
export const addUserName = (user_name) => {
return { type: ADD_USER_NAME, user_name };
};
export const addRank = (rank_info) => {
return { type: ADD_RANK, rank_info };
};
export const addUserMessage = (user_message) => {
return { type: ADD_USER_MESSAGE, user_message };
};
export const getRank = (rank_list) => {
return { type: GET_RANK, rank_list };
};
//Reducer
export default function reducer(state = initialState, action = {}) {
switch (action.type) {
case "rank/ADD_USER_NAME": {
return { ...state, user_name: action.user_name };
}
case "rank/ADD_USER_MESSAGE": {
return { ...state, user_message: action.user_message };
}
case "rank/ADD_RANK": {
return { ...state, ranking: [...state.ranking, action.rank_info] };
}
case "rank/GET_RANK": {
return { ...state, ranking: action.rank_list };
}
default:
return state;
}
}
「100 Quiz」画面でswifeを1回行う場合は、2回発生したエラーを修正する必要があります.
Reference
この問題について([react]第3週開発ログ), 我々は、より多くの情報をここで見つけました https://velog.io/@tnqls1211v/React-week3-devlogテキストは自由に共有またはコピーできます。ただし、このドキュメントのURLは参考URLとして残しておいてください。
Collection and Share based on the CC Protocol