function exampleReducer(state, action) { return state.changedBasedOn(action) }
{ "name": "isomorphic-redux", "version": "1.0.0", "description": "Basic isomorphic redux application", "main": "index.js", "scripts": { "start": "NODE_PATH=$NODE_PATH:./shared node .", "dev": "npm run start & webpack-dev-server --progress --color" }, "author": "<your-name> <<your-email>>", "license": "MIT", "dependencies": { "axios": "^0.5.4", "express": "^4.13.2", "immutable": "^3.7.4", "object-assign": "^3.0.0", "react": "^0.13.3", "react-redux": "^0.2.2", "react-router": "^1.0.0-beta3", "redux": "^1.0.0-rc" }, "devDependencies": { "babel": "^5.8.20", "babel-eslint": "^4.0.5", "babel-loader": "^5.3.2", "eslint": "^1.0.0", "eslint-plugin-react": "^3.1.0", "react-hot-loader": "^1.2.8", "webpack": "^1.10.5", "webpack-dev-server": "^1.10.1" } }
var path = require('path'); var webpack = require('webpack'); module.exports = { entry: [ 'webpack-dev-server/client?http://localhost:8080/', 'webpack/hot/only-dev-server', './client' ], output: { path: path.join(__dirname, 'dist'), filename: 'bundle.js' }, resolve: { modulesDirectories: ['node_modules', 'shared'], extensions: ['', '.js', '.jsx'] }, module: { loaders: [ { test: /\.jsx?$/, exclude: /node_modules/, loaders: ['react-hot', 'babel'] } ] }, plugins: [ new webpack.HotModuleReplacementPlugin(), new webpack.NoErrorsPlugin() ], devtool: 'inline-source-map', devServer: { hot: true, proxy: { '*': 'http://localhost:' + (process.env.PORT || 3000) } } };
{ "optional": ["es7.decorators", "es7.classProperties", "es7.objectRestSpread"] }
npm i
to download all the dependent modules, and you can start.
client/ shared/ index.js server.jsx
shared
directory, but you need some gluing code to separate the client and server parts.
'use strict'; require('babel/register')({}); var server = require('./server'); const PORT = process.env.PORT || 3000; server.listen(PORT, function () { console.log('Server listening on', PORT); });
import express from 'express'; const app = express(); app.use((req, res) => { const HTML = ` <DOCTYPE html> <html> <head> <meta charset="utf-8"> <title>Isomorphic Redux Demo</title> </head> <body> <div id="react-view"></div> <script type="application/javascript" src="/bundle.js"></script> </body> </html> `; res.end(HTML); }); export default app;
shared/components/index.jsx
, which integrates our React Router. In this way, we can add aesthetics to the entire application (cap and basement, for example), good architecture for a brilliant SPA.
import React from 'react'; export default class AppView extends React.Component { render() { return ( <div id="app-view"> <h1>Todos</h1> <hr /> {this.props.children} </div> ); } }
import React from 'react'; import { Route } from 'react-router'; import App from 'components'; export default ( <Route name="app" component={App} path="/"> </Route> );
components/index
along the path '/'. Sounds good!
import React from 'react'; import { Router } from 'react-router'; import Location from 'react-router/lib/Location'; import routes from 'routes'; app.use((req, res) => { const location = new Location(req.path, req.query); Router.run(routes, location, (err, routeState) => { if (err) return console.error(err); const InitialComponent = ( <Router {...routeState} /> ); const componentHTML = React.renderToString(InitialComponent); const HTML = `...`; res.end(HTML); }); });
routeState
variable and be able to display the route we’re asking for. Then we can use the deft method renderToString
from React , which will output our component to an HTML string, which we will give to the client in the react-view div we wrote earlier.
<div id="react-view">${componentHTML}</div>
npm start
we will see here http: // localhost: 3000 / that the route is inserted into HTML.
bundle.js
, but we have not yet set the entry point for webpack, which is why such a garbage.
client/index.jsx
and write something:
import React from 'react'; import { Router } from 'react-router'; import { history } from 'react-router/lib/BrowserHistory'; import routes from 'routes'; React.render( <Router children={routes} history={history} />, document.getElementById('react-view') );
/#
URL from the address.
npm run dev
and the Webpack will handle our bundle.js . It looks not so interesting, but going to http: // localhost: 8080 / should all work without errors. Routing is done, and we are ready for the Redux action.
export function createTodo(text) { return { type: 'CREATE_TODO', text, date: Date.now() } } export function editTodo(id, text) { return { type: 'EDIT_TODO', id, text, date: Date.now() }; } export function deleteTodo(id) { return { type: 'DELETE_TODO', id }; }
import { List } from 'immutable'; const defaultState = new List(); export default function todoReducer(state = defaultState, action) { switch(action.type) { case 'CREATE_TODO': return state.concat(action.text); case 'EDIT_TODO': return state.set(action.id, action.text); case 'DELETE_TODO': return state.delete(action.id); default: return state; } }
(state, action) => newState
.reducers/index.js
export { default as todos } from './TodoReducer';
react-redux
has several react-redux
that will help us with this.
import { createStore, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import * as reducers from 'reducers'; app.use((req, res, next) => { const location = new Location(req.path, req.query); const reducer = combineReducers(reducers); const store = createStore(reducer); Router.run(routes, location, (err, routeState) => { if (err) return console.error(err); const InitialView = ( <Provider store={store}> {() => <Router {...routeState} /> } </Provider> );
Provider
.
const initialState = store.getState();
<title>Redux Demo</title> <script type="application/javascript"> window.__INITIAL_STATE__ = ${JSON.stringify(initialState)}; </script>
window.__INITIAL_STATE__
, not bad, huh?
import { createStore, combineReducers } from 'redux'; import { Provider } from 'react-redux'; import * as reducers from 'reducers'; import { fromJS } from 'immutable'; let initialState = window.__INITIAL_STATE__; // Transform into Immutable.js collections, // but leave top level keys untouched for Redux Object .keys(initialState) .forEach(key => { initialState[key] = fromJS(initialState[key]); }); const reducer = combineReducers(reducers); const store = createStore(reducer, initialState); React.render( <Provider store={store}> {() => <Router children={routes} history={history} /> } </Provider>, document.getElementById('react-view'); );
import React from 'react'; import TodosView from 'components/TodosView'; import TodosForm from 'components/TodosForm'; import { bindActionCreators } from 'redux'; import * as TodoActions from 'actions/TodoActions'; import { connect } from 'react-redux'; @connect(state => ({ todos: state.todos })) export default class Home extends React.Component { render() { const { todos, dispatch } = this.props; return ( <div id="todo-list"> <TodosView todos={todos} {...bindActionCreators(TodoActions, dispatch)} /> <TodosForm {...bindActionCreators(TodoActions, dispatch)} /> </div> ); } }
<Connector>
) that gives access to the requested parts of the state as component properties, therefore we can use todos
, which we do. It also gives access to the dispatch
function from Redux, with which we can process our actions, like this:
dispatch(actionCreator());
bindActionCreators
function from Redux to forward the associated action creators.
dispatch()
function.
import React from 'react'; export default class TodosView extends React.Component { handleDelete = (e) => { const id = Number(e.target.dataset.id); // Equivalent to `dispatch(deleteTodo())` this.props.deleteTodo(id); } handleEdit = (e) => { const id = Number(e.target.dataset.id); const val = this.props.todos.get(id).text // For cutting edge UX let newVal = window.prompt('', val); this.props.editTodo(id, newVal); } render() { return ( <div id="todo-list"> { this.props.todos.map( (todo, index) => { return ( <div key={index}> <span>{todo}</span> <button data-id={index} onClick={this.handleDelete}> X </button> <button data-id={index} onClick={this.handleEdit}> Edit </button> </div> ); }) } </div> ); } }
Note that you can also use React.createClass to avoid problems, and use mixins, although I prefer to use the ES6 classes for cleanliness and consistency.
import React from 'react'; export default class TodosForm extends React.Component { handleSubmit = () => { let node = this.refs['todo-input'].getDOMNode(); this.props.createTodo(node.value); node.value = ''; } render() { return ( <div id="todo-form"> <input type="text" placeholder="type todo" ref="todo-input" /> <input type="submit" value="OK!" onClick={this.handleSubmit} /> </div> ); } }
import Home from 'components/Home'; export default ( <Route name="app" component={App} path="/"> <Route component={Home} path="home" /> </Route> );
export default function promiseMiddleware() { return next => action => { const { promise, type, ...rest } = action; if (!promise) return next(action); const SUCCESS = type; const REQUEST = type + '_REQUEST'; const FAILURE = type + '_FAILURE'; next({ ...rest, type: REQUEST }); return promise .then(res = > { next({ ...rest, res, type: SUCCESS }); return true; }) .catch(error => { next({ ...rest, error, type: FAILURE }); // Another benefit is being able to log all failures here console.log(error); return false; }); }; }
... import { applyMiddleware } from 'redux'; import promiseMiddleware from 'lib/promiseMiddleware'; ... const store = applyMiddleware(promiseMiddleware)(createStore)(reducer);
import request from 'axios'; const BACKEND_URL = 'https://webtask.it.auth0.com/api/run/wt-milomord-gmail_com-0/redux-tutorial-backend?webtask_no_cache=1'; export function createTodo(text) { return { type: 'CREATE_TODO', promise: request.post(BACKEND_URL, { text }) } }
return state.concat(action.res.data.text);
export function getTodos() { return { type: 'GET_TODOS', promise: request.get(BACKEND_URL) } }
case 'GET_TODOS': return state.concat(action.res.data);
componentDidMount() { this.props.getTodos(); }
static needs = [ TodoActions.getTodos ]
export default function fetchComponentData(dispatch, components, params) { const needs = components.reduce( (prev, current) => { return (current.needs || []) .concat((current.WrappedComponent ? current.WrappedComponent.needs : []) || []) .concat(prev); }, []); const promises = needs.map(need => dispatch(need(params))); return Promise.all(promises); }
import fetchComponentData from 'lib/fetchComponentData'; Router.run(routes, location, (err, routeState) => { if (err) return console.error(err); function renderView() { // ... Rest of the old code goes here return HTML; } // Check this is rendering *something*, for safety if(routeState) fetchComponentData(store.dispatch, routeState.components, routeState.params) .then(renderView) .then(html => res.end(html)) .catch(err => res.end(err.message)); });
npm run dev
to update the changes on the server.
Source: https://habr.com/ru/post/264423/