Developing an isomorphic application through the eyes of my wife
This is a continuation of an article about developing an isomorphic application from scratch to React.js . In this part we will add some pages, bootstrap , routing, the concept of Flux and its popular implementation of Redux .
1) We assemble the base stack of an isomorphic application
2) We make a simple application with routing and bootstrap
3) We implement interaction with API and authorization
So, in the first part we ended up developing a simple HelloWorld component and putting together an environment for building and quality control of the code. It is time to make a full-fledged site, which means that we will add a few more pages, link them with links and implement isomorphic routing.
This is a very popular library that allows you to use React -style bootstrap elements.
For example, instead of constructions like
<div className="nav navbar">
we can write
<Nav navbar>
You also do not have to use the JavaScript code of the original bootstrap , because it is already implemented in the components react-bootstrap .
npm i --save react-bootstrap
Select the HelloWorld widget from App.jsx into a separate component. I recall that App.jsx is the entry point to the isomorphic part of the application, and we will soon rewrite it in the form of a layout, inside which the pages requested by the user will be displayed.
mkdir src/components/HelloWorldPage mv src/components/App.jsx src/components/HelloWorldPage/HelloWorldPage.jsx mv src/components/App.css src/components/HelloWorldPage/HelloWorldPage.css
--- import './App.css'; +++ import './HelloWorldPage.css';
import HelloWorldPage from './HelloWorldPage'; export default HelloWorldPage;
This step will allow us to import our component so
import HelloWorldPage from 'components/HelloWorldPage';
instead
import HelloWorldPage from 'components/HelloWorldPage/HelloWorldPage';
It is neater and simplifies the maintenance of the source code of the application.
mkdir src/components/App
import App from './App'; export default App;
import React, { Component } from 'react'; import Grid from 'react-bootstrap/lib/Grid'; import Nav from 'react-bootstrap/lib/Nav'; import Navbar from 'react-bootstrap/lib/Navbar'; import NavItem from 'react-bootstrap/lib/NavItem'; import HelloWorldPage from 'components/HelloWorldPage'; class App extends Component { render() { return ( <div> <Navbar> <Navbar.Header> <Navbar.Brand> <span>Hello World</span> </Navbar.Brand> <Navbar.Toggle /> </Navbar.Header> <Navbar.Collapse> <Nav navbar> <NavItem></NavItem> <NavItem></NavItem> </Nav> </Navbar.Collapse> </Navbar> <Grid> <HelloWorldPage /> </Grid> </div> ); } } export default App;
Important note: note that I explicitly indicate which components of the react-bootstrap I am importing. This will help the webpack in the build process to include only the react-bootstrap part used in the project, and not the entire library, as it would have happened if I had written
import { Grid, Nav, Navbar, NavItem } from 'react-bootstrap';
It is important to note that this maneuver only works in cases where the library used supports modularity. For example, react-bootstrap and lodash refer to such, but jquery and momentjs do not.
As can be seen from the code, the above component does not work with the state and does not use component workflow callbacks (for example, componentWillMount and componentDidMount ). This means that it can be rewritten in the form of the so-called Pure Sateless Function Component .
In the future, components written in this way will have an advantage in performance (thanks to the functional programming theory and the concept of pure functions ), and the more productive each component is, the more productive the application will be in the end.
In the meantime, the reactor wraps such components in the usual ES6-classes, but with one nice bonus:
By default, the component is always updated when new props and / or state values ​​are received , even in cases where they completely coincide with the previous ones. This is not always necessary. A developer has the opportunity to independently implement the method shouldComponentUpdate (nextProps, nextState) , which returns either true or false . With the help of it, you yourself can explicitly tell the React, in which cases you want the component to be redrawn, and in which cases it is not.
If the component is implemented as the Pure Stateless Function Component , then the React is itself able to determine the need to update the appearance of the component without the obvious implementation of shouldComponentUpdate , that is, we get more profit with less effort.
Note: The code below is an example of such a component. Since we will make changes to App.jsx in the future , and it will cease to be a pure stateless component, this example should not be transferred to our project.
Note 2: in our project I will implement all components in the form of ES6-classes, even where it would be possible and correct to implement them in the form of Pure Stateless Functions Components , so as not to complicate the content of the article.
import React from 'react'; import Grid from 'react-bootstrap/lib/Grid'; import Nav from 'react-bootstrap/lib/Nav'; import Navbar from 'react-bootstrap/lib/Navbar'; import NavItem from 'react-bootstrap/lib/NavItem'; import HelloWorldPage from './HelloWorldPage'; function App() { return ( <div> <Navbar> <Navbar.Header> <Navbar.Brand> <span>Hello World</span> </Navbar.Brand> <Navbar.Toggle /> </Navbar.Header> <Navbar.Collapse> <Nav navbar> <NavItem></NavItem> <NavItem></NavItem> </Nav> </Navbar.Collapse> </Navbar> <Grid> <HelloWorldPage /> </Grid> </div> ); } export default App;
It's time to see what has changed in the browser. And ... yes, bootstrap has no styles. The developers of react-bootstrap deliberately did not include them in the distribution, as all the same you will use your own theme. Therefore, we go to any site with themes for bootstrap , for example bootswatch.com , and download the one you like. Save it in src / components / App / bootstrap.css. I recommend saving the full version, since it is easier to customize it, and then the webpack will still make the minification anyway .
Note: you can download my theme from the repository on github .
Let's make a change in App.jsx
+++ import './bootstrap.css';
I do not want to focus now on setting up work with glyphicons , especially since we will not use them in the project, so just remove them from the styles.
--- @font-face { --- font-family: 'Glyphicons Halflings'; --- src: url('../fonts/glyphicons-halflings-regular.eot'); src: url('../fonts/glyphicons-halflings-regular.eot?#iefix') format('embedded-opentype'), url('../fonts/glyphicons-halflings-regular.woff2') format('woff2'), url('../fonts/glyphicons-halflings-regular.woff') format('woff'), url('../fonts/glyphicons-halflings-regular.ttf') format('truetype'), url('../fonts/glyphicons-halflings-regular.svg#glyphicons_halflingsregular') format('svg'); --- }
Back in the browser, and now everything should look good.
Note: if you are annoyed when reloading the page, that the old version of the page first appears, and the new one only after a couple of seconds - just restart nodemon .
import CounterPage from './CounterPage'; export default CounterPage;
import React, { Component } from 'react'; class CounterPage extends Component { render() { return <div> </div>; } } export default CounterPage;
import TimePage from './TimePage'; export default TimePage;
import React, { Component } from 'react'; class TimePage extends Component { render() { return <div> </div>; } } export default TimePage;
For routing, we will use the react-router library.
npm i --save react-router
To make it work, you need to make the following changes to our project:
import React from 'react'; import { IndexRoute, Route } from 'react-router'; import App from 'components/App'; import CounterPage from 'components/CounterPage'; import HelloWorldPage from 'components/HelloWorldPage'; import TimePage from 'components/TimePage'; export default ( <Route component={App} path='/'> <IndexRoute component={HelloWorldPage} /> <Route component={CounterPage} path='counters' /> <Route component={TimePage} path='time' /> </Route> );
Please note that we are in fact exporting the React component. IndexRoute is analogous to index.html or index.php on the web: if part of the path is omitted, then it will be chosen.
Note: The Route and IndexRoute components can be nested in other Routes as many times as necessary. In our example, we limit ourselves to two levels.
URL '/' => component of the form <HelloWorldPage />
URL '/ counter' => <CounterPage />
URL '/ time' => <TimePage />
In our application, the App component should play the role of the layout, so you need to “teach” it to render the nested ( children ) components.
--- import React, { Component } from 'react'; +++ import React, { Component, PropTypes } from 'react'; import Grid from 'react-bootstrap/lib/Grid'; import Nav from 'react-bootstrap/lib/Nav'; import Navbar from 'react-bootstrap/lib/Navbar'; import NavItem from 'react-bootstrap/lib/NavItem'; --- import HelloWorldPage from 'components/HelloWorldPage'; import './bootstrap.css'; +++ const propTypes = { +++ children: PropTypes.node +++ }; class App extends Component { render() { return ( <div> <Navbar> <Navbar.Header> <Navbar.Brand> <span>Hello World</span> </Navbar.Brand> <Navbar.Toggle /> </Navbar.Header> <Navbar.Collapse> <Nav navbar> <NavItem></NavItem> <NavItem></NavItem> </Nav> </Navbar.Collapse> </Navbar> <Grid> +++ {this.props.children} --- <HelloWorldPage /> </Grid> </div> ); } } +++ App.propTypes = propTypes; export default App;
--- import App from 'components/App'; +++ import { match, RouterContext } from 'react-router'; +++ import routes from './routes'; app.use((req, res) => { --- const componentHTML = ReactDom.renderToString(<App />); +++ match({ routes, location: req.url }, (error, redirectLocation, renderProps) => { +++ if (redirectLocation) { // redirect +++ return res.redirect(301, redirectLocation.pathname + redirectLocation.search); +++ } +++ if (error) { // +++ return res.status(500).send(error.message); +++ } +++ if (!renderProps) { // , URL +++ return res.status(404).send('Not found'); +++ } +++ const componentHTML = ReactDom.renderToString(<RouterContext {...renderProps} />); +++ return res.end(renderHTML(componentHTML)); +++ }); --- return res.end(renderHTML(componentHTML)); });
Note: The match function takes as its first parameter a JavaScript object with the routes and location keys. I am using shorthand notation ES6 , the full version would look like this
{ routes: routes, location: req.url},
where we import routes from the routes.jsx file. As the second parameter match takes the callback function, which is responsible for rendering.
Temporarily disable client javascript
// ReactDOM.render(<App />, document.getElementById('react-view'));
Now let's test the routing in the browser - our page looks the same, even though we got rid of the explicit attachment of the HelloWorldPage component in the App container. Moving on.
import { Link } from 'react-router'; <Link to='/my-fancy-path'>Link text</Link>
However, we need to arrange the NavItem components as links. To do this, use the react-router-bootstrap library.
npm i --save react-router-bootstrap
+++ import { Link } from 'react-router'; +++ import LinkContainer from 'react-router-bootstrap/lib/LinkContainer'; --- <span>Hello World</span> +++ <Link to='/'>Hello World</Link> --- <NavItem></NavItem> +++ <LinkContainer to='/time'> +++ <NavItem></NavItem> +++ </LinkContainer> --- <NavItem></NavItem> +++ <LinkContainer to='/counters'> +++ <NavItem></NavItem> +++ </LinkContainer>
Test server routing.
Restart nodemon . In the browser, open the Developer Tools , Network tab.
Now you can evaluate the results of our work and click on the links in the navigation menu. Note that the requests go to the server where the express is processed. It, in turn, renders and returns to the browser the HTML code of the requested page. Now our application works exactly like a classic web application.
If client-side JavaScript does not have time to load and initialize, or there is an error in it, our application will still work successfully, as we just could see.
--- import App from 'components/App'; +++ import { browserHistory, Router } from 'react-router'; +++ import routes from './routes'; --- // ReactDOM.render(<App />, document.getElementById('react-view')); +++ const component = ( +++ <Router history={browserHistory}> +++ {routes} +++ </Router> +++ ); +++ ReactDOM.render(component, document.getElementById('react-view'));
Note: Please note that now the Router component has become the root component of our application. It tracks URL changes and generates page content based on the routes we configured.
Back in the browser and click on the links again, carefully watching the Developer Tools Network tab. This time the page does not reload, requests to the server do not go away, and client-side JavaScript renders the requested page time after time. Everything is working!
We added several pages and successfully configured client and server routing, making sure that they work correctly for all scenarios.
First, we will implement a page with "counters" to talk about Flux and Redux as close as possible to practice.
Create two new components: Counter.jsx and StateCounter.jsx .
Counter will display the value passed to it and the plus button, which is responsible for changing this value.
StateCounter is the parent component of the Counter component. It will store the current Counter value in its own state storage and contain the business logic for updating this value when the user clicks the plus button.
I deliberately chose this implementation to explicitly separate the interface and business logic.
This technique is very often used in practice, since such code is simpler:
In particular, in our project several components will use Counter at once.
import React, { Component, PropTypes } from 'react'; import Button from 'react-bootstrap/lib/Button'; import './Counter.css'; const propTypes = { onClick: PropTypes.func, value: PropTypes.number }; const defaultProps = { onClick: () => {}, value: 0 }; class Counter extends Component { render() { const { onClick, value } = this.props; return ( <div> <div className='counter-label'> Value: {value} </div> <Button onClick={onClick}>+</Button> </div> ); } } Counter.propTypes = propTypes; Counter.defaultProps = defaultProps; export default Counter;
.counter-label { display: inline-block; margin-right: 20px; }
import React, { Component } from 'react'; import Counter from './Counter'; class StateCounter extends Component { constructor() { super(); this.handleClick = this.handleClick.bind(this); this.state = { value: 0 }; } handleClick() { this.setState({ value: this.state.value + 1 }); } render() { return <Counter value={this.state.value} onClick={this.handleClick} />; } } export default StateCounter;
+++ import PageHeader from 'react-bootstrap/lib/PageHeader'; +++ import StateCounter from './StateCounter'; render() { --- return <div> </div>; +++ return ( +++ <div> +++ <PageHeader>Counters</PageHeader> +++ <h3>State Counter</h3> +++ <StateCounter /> +++ </div> +++ ); }
It's time to test the updated code. In the browser, go to the "Counters" tab and click on the "+" button. The value has changed from 0 to 1. Great! Now go to any other tab, and then go back. The counter value again became "0". This is highly expected, but does not always correspond to what we would like to see.
It is time to discuss the concept of "Flux".
Note: Flux is a concept, not a library. Today, there are many different libraries that implement it.
Components do not contain business logic, but are responsible only for rendering the interface.
There is one object in the application that stores the state of the entire application. I will call it a “global state,” although I'm not quite sure that this is the most successful term. At the request of the developer, some components "subscribe" to the part of the global state they are interested in. Over time, the global state may change, and all components subscribed to it receive updates automatically.
Important note: the global state only describes the state of your front-end application in a separate tab and is stored exclusively in the RAM memory of the browser. Thus, it will be lost if the user presses F5, which is absolutely normal, expected by design. I will focus on this topic in more detail in the third part.
Suppose we have an online store site: in the center of the page we will see a list of products, in the navigation bar there is a basket icon with the quantity of goods and their total cost, and somewhere on the right there is a block with details of the goods added to the basket. In short, a fairly common scenario.
If we wrote this script on jQuery , then we would have to write a lot of code to work with DOM. In the process of implementing all new customer requirements, the code would become more complicated, and, with a high degree of probability, something would break in the end, and the complexity and cost of support would constantly increase with the passage of time and new "hoteles".
Note: the "Add to Cart", "Notifications", "Basket" and "Cart Detail" components are subscribed to a global state.
loading
value equal to true , which makes it off, and its icon changes to the download indicator according to the source code of this component.message
value, which makes it visible to the user, the Recycle component receives the prop count
values ​​with the new quantity of goods and the prop value
with the order amount, the Recycle Bin component receives the value prop items
- an updated list of objects corresponding to all products added to the cart. If in the future the customer wishes that something else happens on the page, we can easily bring it to life without changing either the code of other components or the function that performs the business logic. We need only implement a new component and indicate in it what part of the global state we are interested in.message
value and show the user an informational message.loading
value false . The button will return to its original state. function addItemToCart(itemId) { return (dispatch) => { dispatch(addItemToCartStarted(itemId)); addItemToCartAPICall(itemId) .then( (data) => { dispatch(itemToCardAdded(data)); dispatch(addItemToCartFinished(data)); } ) .catch(error => dispatch(addItemToCartFailed(itemId, error))); } }
Simplified, though not entirely correct: in this example, the dispatch function is responsible for updating the global state. We take a function that contains the business logic of updating the global state, and pass it as the first argument to the dispatch function.
.
<Button onClick={() => dispatch(addItemToCart(3))} />
.
!
: Flux .
Pros:
Minuses:
: : " ?" " ?". Nothing! , . , . , - , ! , , , , !
npm i --save redux react-redux redux-thunk
3.2.2.1 src/redux , src/redux/actions src/redux/reducers .
3.2.2.2 counterActions.js . , .
export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; export function incrementCounter() { return { type: INCREMENT_COUNTER }; }
3.2.2.3 counterReducer.js . .
import { INCREMENT_COUNTER } from 'redux/actions/counterActions'; const initialState = { value: 0 }; export default function(state = initialState, action) { switch (action.type) { case INCREMENT_COUNTER: return { value: state.value + 1 }; default: return state; } }
: , redux ( "") @@INIT , .
configureStore.js
import { applyMiddleware, combineReducers, createStore } from 'redux'; import thunk from 'redux-thunk'; import counterReducer from './reducers/counterReducer'; export default function (initialState = {}) { const rootReducer = combineReducers({ counter: counterReducer }); return createStore(rootReducer, initialState, applyMiddleware(thunk)); }
+++ import { Provider } from 'react-redux'; +++ import configureStore from './redux/configureStore'; app.use((req, res) => { +++ const store = configureStore(); ... --- const componentHTML = ReactDom.renderToString(<RouterContext {...renderProps} />); +++ const componentHTML = ReactDom.renderToString( +++ <Provider store={store}> +++ <RouterContext {...renderProps} /> +++ </Provider> +++ );
— props . props , child child . , , , , API , , .
, , . API , , , .
Provider react-redux store. , , , .
+++ import { Provider } from 'react-redux'; +++ import configureStore from './redux/configureStore'; +++ const store = configureStore(); const component = ( +++ <Provider store={store}> <Router history={browserHistory}> {routes} </Router> +++ </Provider> );
import React, { Component, PropTypes } from 'react'; import { connect } from 'react-redux'; import Counter from './Counter'; import { incrementCounter } from 'redux/actions/counterActions'; const propTypes = { dispatch: PropTypes.func.isRequired, value: PropTypes.number.isRequired }; class ReduxCounter extends Component { constructor(props) { super(props); this.handleClick = this.handleClick.bind(this); } handleClick() { this.props.dispatch(incrementCounter()); } render() { return <Counter value={this.props.value} onClick={this.handleClick} />; } } ReduxCounter.propTypes = propTypes; function mapStateToProps(state) { const { value } = state.counter; return { value }; } export default connect(mapStateToProps)(ReduxCounter);
connect . :
: connect High Order Components HOCs .
High Order Component : , , , , , props .
connect .
function connect(mapStateToProps) { function dispatch(...) { ... } const injectedProps = mapStateToProps(globalState); return (WrappedComponent) => { class HighOrderComponent extends Component { render() { <WrappedComponent {...this.props} {...injectedProps} dispatch={dispatch} />; } }; return HighOrderComponent; } }
+++ import ReduxCounter from './ReduxCounter'; <StateCounter /> +++ <h3>Redux Counter</h3> +++ <ReduxCounter />
, , !
github — https://github.com/yury-dymov/habr-app/tree/v2 .
Ps , , . Thank you in advance!
Source: https://habr.com/ru/post/310284/
All Articles