High Order Component
Controlled Component 와 Uncontrolled Component
[Project] Login 요청하기
localStorage
Rest API 로 데이터 가져오기
[Homework] 개발 서적 평가 서비스 책 추가하기
Lead Software Engineer @ProtoPie
Microsoft MVP
TypeScript Korea User Group Organizer
Marktube (Youtube)
이 웅재
Higher Order Component
advanced technique in React for reusing component logic.
not part of the React API
a pattern that emerges from React’s compositional nature.
HOC = function(컴포넌트) { return 새로운 컴포넌트; }
import React from "react";
import { withRouter } from "react-router-dom";
const LoginButton = props => {
console.log(props);
function login() {
setTimeout(() => {
props.history.push("/");
}, 1000);
}
return <button onClick={login}>로그인하기</button>;
};
export default withRouter(LoginButton);
보통 with 가 붙은 함수가 HOC 인 경우가 많다.
export default withRouter(LoginButton);
Use HOCs For Cross-Cutting Concerns
Don’t Mutate the Original Component. Use Composition.
Pass Unrelated Props Through to the Wrapped Component
Maximizing Composability
Wrap the Display Name for Easy Debugging
Don’t Use HOCs Inside the render Method
Static Methods Must Be Copied Over
Refs Aren’t Passed Through (feat. React.forwardRef)
render() {
// A new version of EnhancedComponent is created on every render
// EnhancedComponent1 !== EnhancedComponent2
const EnhancedComponent = enhance(MyComponent);
// That causes the entire subtree to unmount/remount each time!
return <EnhancedComponent />;
}
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
// Must know exactly which method(s) to copy :(
Enhance.staticMethod = WrappedComponent.staticMethod;
return Enhance;
}
// Define a static method
WrappedComponent.staticMethod = function() {/*...*/}
// Now apply a HOC
const EnhancedComponent = enhance(WrappedComponent);
// The enhanced component has no static method
typeof EnhancedComponent.staticMethod === 'undefined' // true
import hoistNonReactStatic from 'hoist-non-react-statics';
function enhance(WrappedComponent) {
class Enhance extends React.Component {/*...*/}
hoistNonReactStatic(Enhance, WrappedComponent);
return Enhance;
}
// Instead of...
MyComponent.someFunction = someFunction;
export default MyComponent;
// ...export the method separately...
export { someFunction };
// ...and in the consuming module, import both
import MyComponent, { someFunction } from './MyComponent.js';
npx create-react-app controlled-uncontrolled
import React from 'react';
export default class Controlled extends React.Component {
state = { value: '' };
render() {
return (
<div>
<input />
</div>
);
}
}
import React from 'react';
export default class Controlled extends React.Component {
state = { value: '' };
render() {
const { value } = this.state;
return (
<div>
<input value={value} onChange={this._change} />
</div>
);
}
_change = e => {
// console.log(e.target.value);
this.setState({ value: e.target.value });
};
}
import React from 'react';
export default class Controlled extends React.Component {
state = { value: '' };
render() {
const { value } = this.state;
return (
<div>
<input value={value} onChange={this._change} />
<button onClick={this._click}>전송</button>
</div>
);
}
_change = e => {
// console.log(e.target.value);
this.setState({ value: e.target.value });
};
_click = () => {
console.log('최종 결과', this.state.value);
};
}
import React from 'react';
export default class Uncontrolled extends React.Component {
_input = React.createRef();
render() {
return (
<div>
<input ret={this._input} />
</div>
);
}
}
import React from 'react';
export default class Uncontrolled extends React.Component {
_input = React.createRef();
render() {
return (
<div>
<input ref={this._input} />
<button onClick={this._click}>전송</button>
</div>
);
}
_click = () => {
console.log('최종 결과', this._input.current.value);
};
}
import React from 'react';
const Uncontrolled = () => {
const inputRef = React.createRef();
function click() {
console.log('최종 결과', inputRef.current.value);
}
return (
<div>
<input ref={inputRef} />
<button onClick={click}>전송</button>
</div>
);
};
export default Uncontrolled;
git clone -b fc-school https://github.com/2woongjae/reactjs-books-review.git
// src/components/SignForm.jsx
import React from "react";
import { Input, Button, Divider, Col } from "antd";
import styled from "styled-components";
import { Link } from "react-router-dom";
const SigninForm = () => {
const emailInput = React.createRef();
const passwordInput = React.createRef();
function click() {
console.log(
emailInput.current.state.value,
passwordInput.current.state.value
);
}
return (
<Col
span={12}
style={{
verticalAlign: "top"
}}
>
<form>
<Title>Log In. Start Searching.</Title>
<InputTitle>
Email
<StyledSpan />
</InputTitle>
<InputArea>
<StyledInput
placeholder="Email"
autoComplete="email"
name="email"
ref={emailInput}
/>
</InputArea>
<InputTitle top={10}>
Password
<StyledSpan />
</InputTitle>
<InputArea>
<StyledInput
type="password"
autoComplete="current-password"
ref={passwordInput}
/>
</InputArea>
<ButtonArea>
<StyledButton size="large" loading={loading} onClick={click}>
Sign In
</StyledButton>
</ButtonArea>
<DividerArea>
<Divider />
</DividerArea>
<LinkArea>
<LinkTitle>Need to create an account?</LinkTitle>
<LinkButtonArea>
<Link to="/signup">
<LinkButton>Sign up</LinkButton>
</Link>
</LinkButtonArea>
</LinkArea>
<LinkArea>
<LinkTitle>Forgot your password?</LinkTitle>
<LinkButtonArea>
<Link to="/forgot">
<LinkButton>Recovery</LinkButton>
</Link>
</LinkButtonArea>
</LinkArea>
</form>
</Col>
);
};
export default SigninForm;
const Title = styled.div`
padding-top: 10px;
padding-bottom: 10px;
text-transform: uppercase;
font-family: Roboto;
font-size: 24px;
font-weight: bold;
margin-top: 60px;
text-align: center;
`;
const InputTitle = styled.div`
font-family: Roboto;
font-size: 12px;
font-weight: bold;
margin-top: ${props => props.top || "40"}px;
text-align: left;
padding-left: 40px;
`;
const InputArea = styled.div`
padding-top: 10px;
padding-bottom: 10px;
padding-left: 40px;
padding-right: 40px;
`;
const StyledInput = styled(Input)`
width: 100%;
border-radius: 1px;
border-width: 1px;
font-family: Roboto;
`;
const ButtonArea = styled.div`
text-align: left;
padding-left: 40px;
margin-top: 20px;
`;
const StyledButton = styled(Button)`
border-color: #28546a;
background-color: #28546a;
text-transform: uppercase;
border-radius: 1px;
border-width: 2px;
color: white;
width: 120px;
&:hover {
background-color: #28546a;
color: white;
}
`;
const DividerArea = styled.div`
font-family: Roboto;
font-size: 12px;
font-weight: bold;
margin-top: 30px;
text-align: left;
padding-left: 40px;
padding-right: 40px;
`;
const LinkArea = styled.div`
padding-left: 40px;
padding-right: 40px;
margin-top: 15px;
overflow: hidden;
`;
const LinkTitle = styled.div`
float: left;
padding-top: 5px;
`;
const StyledSpan = styled.span.attrs(() => ({
children: "*"
}))`
color: #971931;
`;
const LinkButtonArea = styled.div`
float: right;
`;
const LinkButton = styled(Button)`
background-color: #f3f7f8;
border-color: #28546a;
color: #28546a;
text-transform: uppercase;
border-radius: 1px;
border-width: 2px;
&:hover {
background-color: #28546a;
color: white;
}
`;
npm i axios
// ...
import axios from 'axios';
const SigninForm = () => {
// ...
function click() {
const email = emailInput.current.state.value;
const password = passwordInput.current.state.value;
axios
.post('https://api.marktube.tv/v1/me', {
email,
password,
})
.then(function(response) {
console.log(response);
})
.catch(function(error) {
console.log(error);
});
}
// ...
};
// ...
import axios from 'axios';
const SigninForm = () => {
// ...
async function click() {
const email = emailInput.current.state.value;
const password = passwordInput.current.state.value;
try {
const response = await axios.post('https://api.marktube.tv/v1/me', {
email,
password,
});
console.log(response);
} catch (error) {
console.log(error);
}
}
// ...
};
import { withRouter } from 'react-router-dom';
const SigninForm = ({ history }) => {
// ...
async function click() {
const email = emailInput.current.state.value;
const password = passwordInput.current.state.value;
try {
const response = await axios.post('https://api.marktube.tv/v1/me', {
email,
password,
});
console.log(response);
history.push('/');
} catch (error) {
console.log(error);
}
}
// ...
};
export default withRouter(SigninForm);
const SigninForm = ({ history }) => {
// ...
const [loading, setLoading] = useState(false);
async function click() {
const email = emailInput.current.state.value;
const password = passwordInput.current.state.value;
try {
setLoading(true);
const response = await axios.post('https://api.marktube.tv/v1/me', {
email,
password,
});
console.log(response);
setLoading(false);
history.push('/');
} catch (error) {
console.log(error);
setLoading(false);
}
}
// ...
};
const SigninForm = ({ history }) => {
// ...
return (
<Col
span={12}
style={{
verticalAlign: "top"
}}
>
<form>
...
<ButtonArea>
<StyledButton size="large" loading={loading} onClick={click}>
Sign In
</StyledButton>
</ButtonArea>
...
</form>
</Col>
);
};
import { Input, Button, Divider, Col, message } from 'antd';
const SigninForm = ({ history }) => {
// ...
async function click() {
const email = emailInput.current.state.value;
const password = passwordInput.current.state.value;
try {
setLoading(true);
const response = await axios.post("https://api.marktube.tv/v1/me", {
email,
password
});
console.log(response);
setLoading(false);
history.push("/");
} catch (error) {
console.log(error);
setLoading(false);
message.error('This is an error message');
}
}
// ...
};
const SigninForm = ({ history }) => {
// ...
async function click() {
const email = emailInput.current.state.value;
const password = passwordInput.current.state.value;
try {
setLoading(true);
const response = await axios.post("https://api.marktube.tv/v1/me", {
email,
password
});
console.log(response);
setLoading(false);
history.push("/");
} catch (error) {
console.log(error);
setLoading(false);
message.error(error.response.data.error);
}
}
// ...
};
const SigninForm = ({ history }) => {
// ...
async function click() {
const email = emailInput.current.state.value;
const password = passwordInput.current.state.value;
try {
setLoading(true);
const response = await axios.post("https://api.marktube.tv/v1/me", {
email,
password
});
console.log(response);
setLoading(false);
localStorage.setItem('token', response.data.token);
history.push("/");
} catch (error) {
console.log(error);
setLoading(false);
message.error(error.response.data.error);
}
}
// ...
};
import React from 'react';
import { BrowserRouter, Route, Switch, Redirect } from 'react-router-dom';
import Signin from './pages/Signin';
import Home from './pages/Home';
import Error from './pages/Error';
export default function App() {
return (
<BrowserRouter>
<Switch>
<Route path="/signin" exact component={Signin} />
<Route
path="/"
exact
render={props => {
const token = localStorage.getItem('token');
console.log(token);
if (token === null) {
return <Redirect to="/signin" />;
}
return <Home {...props} token={token} />;
}}
/>
<Route component={Error} />
</Switch>
</BrowserRouter>
);
}
// src/hocs/withAuth.js
export default function withAuth(Component) {
return props => {
const token = localStorage.getItem('token');
console.log(token);
if (token === null) {
return <Redirect to="/signin" />;
}
return <Component {...props} token={token} />;
};
}
import React from 'react';
import axios from 'axios';
function Book(props) {
return <div>title : {props.title}</div>;
}
export default class Home extends React.Component {
state = { books: [] };
render() {
const { books } = this.state;
return (
<div>
<h1>Home</h1>
{books.map(book => (
<Book title={book.title} key={book.bookId} />
))}
</div>
);
}
}
export default class Home extends React.Component {
// ...
async componentDidMount() {
try {
const response = await axios.get('https://api.marktube.tv/v1/book', {
headers: {
Authorization: `Bearer ${this.props.token}`,
},
});
const books = response.data;
console.log(books);
this.setState({ books });
} catch (error) {
console.log(error.response.data.error);
}
}
// ...
}
// 책 목록보기
axios.get(
'https://api.marktube.tv/v1/book',
{ headers: `Bearer ${token}` },
);
// 책 추가하기
axios.post(
'https://api.marktube.tv/v1/book',
{
title,
message,
author,
url,
},
{ headers: `Bearer ${token}` },
);
// 책 상세보기
axios.get(
`https://api.marktube.tv/v1/book/${book.id}`,
{ headers: `Bearer ${token}` },
);
// 책 수정하기
axios.patch(
`https://api.marktube.tv/v1/book/${book.id}`,
{
title,
message,
author,
url,
},
{ headers: `Bearer ${token}` },
);
// 책 삭제하기
axios.delete(
`https://api.marktube.tv/v1/book/${book.id}`,
{ headers: `Bearer ${token}` },
);