Dev

React 공부정리 #6 [END]

September 7, 2016

React 공부정리 #6 [END]

Router Example

  • Udemy 강좌의 ‘Section 4: State with React’에 해당하는 내용
  • Redux에 대한 자세한 내용은 Redux Framework를 참고
  • 한글로 번역된 내용은 Dobbit.Co님의 github을 참고

Setup React Router

$ npm install --save react-router
  • react-router를 설치

src\index.js

import React from 'react';  
import ReactDOM from 'react-dom';  
import { Provider } from 'react-redux';  
import { createStore, applyMiddleware } from 'redux';  
import { Router, browserHistory } from 'react-router';  
import reducers from './reducers';

const createStoreWithMiddleware = applyMiddleware()(createStore);

ReactDOM.render(  
  <Provider store={createStoreWithMiddleware(reducers)}>
    <Router history={browserHistory} />
  </Provider>
  , document.querySelector('.container'));
  • browserHistoy를 사용, 뒤로가기를 해도 필요한 부분만 다시 화면에 표시됨.

src\router.js

import React from 'react';  
import { Route, IndexRoute } from 'react-router';

import App from './components/app';

export default (  
  <Route path="/" component={App}>
  </Route>
);
  • URL의 \ 위치를 지정

Nesting Of Routes

src\routes.js

import React from 'react';  
import { Route, IndexRoute } from 'react-router';

import App from './components/app';

const Greeting = () => {  
  return <div>Hey there!</div>
};

export default (  
  <Route path="/" component={App}>
    <Route path="greet" component={Greeting} />
    <Route path="greet2" component={Greeting} />
    <Route path="greet3" component={Greeting} />
  </Route>
);
  • 다른 URL을 생성하기 위한 짧은 예제

components\app.js

import React from 'react';  
import { Component } from 'react';

export default class App extends Component {  
  render() {
    return (
      <div>
        {this.props.children}
      </div>
    );
  }
}
  • props.children 은 소유자에 의해 자식이 전달되도록 지정

IndexRoutes with React Router

src\routes.js

import React from 'react';  
import { Route, IndexRoute } from 'react-router';

import App from './components/app';  
import PostsIndex from './components/posts_index';

export default (  
  <Route path="/" component={App}>
    <IndexRoute component={PostsIndex} />
  </Route>
);
  • IndexRoute를 설정

actions\index.js

import axios from 'axios';

export const FETCH_POSTS = 'FETCH_POSTS';

const ROOT_URL = 'http://reduxblog.herokuapp.com/api';  
const API_KEY = '?key=lkajdsfapipwietpw';

export function fetchPosts() {  
  const request = axios.get(`${ROOT_URL}/posts${API_KEY}`);

  return {
    type: FETCH_POSTS,
    payload: request
  };
}
  • Promise 기반으로 데이터를 송수신함

reducers\reducer_posts.js

import { FETCH_POSTS, FETCH_POST } from '../actions/index';

const INITIAL_STATE = { all: [], post: null };

export default function(state = INITIAL_STATE, action) {  
  switch(action.type) {
  case FETCH_POSTS:
    return { ...state, all: action.payload.data };
  default:
    return state;
  }
}
  • 데이터를 전달할 reducer를 생성

reducers\index.js

import { combineReducers } from 'redux';  
import PostsReducer from './reducer_posts';  
import { reducer as formReducer } from 'redux-form';

const rootReducer = combineReducers({  
  posts: PostsReducer,
  form: formReducer
});

export default rootReducer;  
  • Reducer를 결합

components/posts_index.js

import React, { Component } from 'react';  
import { connect } from 'react-redux';  
import { fetchPosts } from '../actions/index';

class PostsIndex extends Component {  
  componentWillMount() {
    this.props.fetchPosts();
  }

  render() {
    return (
      <div>List of blog posts</div>
    );
  }
}

export default connect(null, { fetchPosts })(PostsIndex);  
  • Index URL에서 호출할 Component를 생성

New Posts, From, Validation

components/posts_new.js

import React, { Component, PropTypes } from 'react';  
import { reduxForm } from 'redux-form';  
import { createPost } from '../actions/index';  
import { Link } from 'react-router';

class PostsNew extends Component {  
  static contextTypes = {
    router: PropTypes.object
  };

  onSubmit(props) {
    this.props.createPost(props)
      .then(() => {
        this.context.router.push('/');
      });
  }

  render() {
    const { fields: { title, categories, content }, handleSubmit } = this.props;

    return (
      <form onSubmit={handleSubmit(this.onSubmit.bind(this))}>
        <h3>Create A New Post</h3>

        <div className={`form-group ${title.touched && title.invalid ? 'has-danger' : ''}`}>
          <label>Title</label>
          <input type="text" className="form-control" {...title} />
          <div className="text-help">
            {title.touched ? title.error : ''}
          </div>
        </div>

        <div className={`form-group ${categories.touched && categories.invalid ? 'has-danger' : ''}`}>
          <label>Categories</label>
          <input type="text" className="form-control" {...categories} />
          <div className="text-help">
            {categories.touched ? categories.error : ''}
          </div>
        </div>

        <div className={`form-group ${content.touched && content.invalid ? 'has-danger' : ''}`}>
          <label>Content</label>
          <textarea className="form-control" {...content} />
          <div className="text-help">
            {content.touched ? content.error : ''}
          </div>
        </div>

        <button type="submit" className="btn btn-primary">Submit</button>
        <Link to="/" className="btn btn-danger">Cancel</Link>
      </form>
    );
  }
}

function validate(values) {  
  const errors = {};

  if (!values.title) {
    errors.title = 'Enter a username';
  }
  if (!values.categories) {
    errors.categories = 'Enter categories';
  }
  if(!values.content) {
    errors.content = 'Enter some content';
  }

  return errors;
}

 mapDispatchToProps
export default reduxForm({  
  form: 'PostsNewForm',
  fields: ['title', 'categories', 'content'],
  validate
}, null, { createPost })(PostsNew);
  • react-form을 사용해서 데이터를 입력할 ‘폼’을 만들고, 해당 폼의 내용을 검증할 수 있는 validate를 생성

components/posts_index.js

import React, { Component } from 'react';  
import { connect } from 'react-redux';  
import { fetchPosts } from '../actions/index';  
import { Link } from 'react-router';

class PostsIndex extends Component {  
  componentWillMount() {
    this.props.fetchPosts();
  }

  renderPosts() {
    return this.props.posts.map((post) => {
      return (
        <li className="list-group-item" key={post.id}>
          <Link to={"posts/" + post.id}>
            <span className="pull-xs-right">{post.categories}</span>
            <strong>{post.title}</strong>
          </Link>
        </li>
      );
    });
  }

  render() {
    return (
      <div>
        <div className="text-xs-right">
          <Link to="/posts/new" className="btn btn-primary">
            Add a Post
          </Link>
        </div>
        <h3>Posts</h3>
        <ul className="list-group">
          {this.renderPosts()}
        </ul>
      </div>
    );
  }
}

function mapStateToProps(state) {  
  return { posts: state.posts.all };
}

export default connect(mapStateToProps, { fetchPosts })(PostsIndex);  
  • URL Index에 데이터를 연결

src\routes.js

import React from 'react';  
import { Route, IndexRoute } from 'react-router';

import App from './components/app';  
import PostsIndex from './components/posts_index';  
import PostsNew from './components/posts_new';  
import PostsShow from './components/posts_show';

export default (  
  <Route path="/" component={App}>
    <IndexRoute component={PostsIndex} />
    <Route path="posts/new" component={PostsNew} />
    <Route path="posts/:id" component={PostsShow} />
  </Route>
);
  • 동적 파라메터가 자동하는 라우트

Loading Data on Render, Delete!

actions\index.js

import axios from 'axios';

export const FETCH_POSTS = 'FETCH_POSTS';  
export const CREATE_POST = 'CREATE_POST';  
export const FETCH_POST = 'FETCH_POST';  
export const DELETE_POST = 'DELETE_POST';

const ROOT_URL = 'http://reduxblog.herokuapp.com/api';  
const API_KEY = '?key=lkajdsfapipwietpw';

export function fetchPosts() {  
  const request = axios.get(`${ROOT_URL}/posts${API_KEY}`);

  return {
    type: FETCH_POSTS,
    payload: request
  };
}

export function createPost(props) {  
  const request = axios.post(`${ROOT_URL}/posts${API_KEY}`, props);

  return {
    type: CREATE_POST,
    payload: request
  };
}

export function fetchPost(id) {  
  const request = axios.get(`${ROOT_URL}/posts/${id}${API_KEY}`);

  return {
    type: FETCH_POST,
    payload: request
  };
}

export function deletePost(id) {  
  const request = axios.delete(`${ROOT_URL}/posts/${id}${API_KEY}`);

  return {
    type: DELETE_POST,
    payload: request
  };
}
  • Action에 따른 함수 생성

components\posts_show.js

import React, { Component, PropTypes } from 'react';  
import { connect } from 'react-redux';  
import { fetchPost, deletePost } from '../actions/index';  
import { Link } from 'react-router';

class PostsShow extends Component {  
  static contextTypes = {
    router: PropTypes.object
  };

  componentWillMount() {
    this.props.fetchPost(this.props.params.id);
  }

  onDeleteClick() {
    this.props.deletePost(this.props.params.id)
      .then(() => { this.context.router.push('/'); });
  }

  render() {
    const { post } = this.props;

    if (!post) {
      return <div>Loading...</div>;
    }

    return (
      <div>
        <Link to="/">Back To Index</Link>
        <button
          className="btn btn-danger pull-xs-right"
          onClick={this.onDeleteClick.bind(this)}>
          Delete Post
        </button>
        <h3>{post.title}</h3>
        <h6>Categories: {post.categories}</h6>
        <p>{post.content}</p>
      </div>
    );
  }
}

function mapStateToProps(state) {  
  return { post: state.posts.post };
}

export default connect(mapStateToProps, { fetchPost, deletePost })(PostsShow);  
  • URL에 해당하는 데이터를 연결해서 화면에 출력