📜 ⬆️ ⬇️

React.js: build an isomorphic / universal application from scratch. Part 2: add bootstrap, pages and routing


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 .


Table of contents


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.


1. Add to the project react-bootstrap


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 .


Install react-bootstrap


 npm i --save react-bootstrap 

We make changes to the project


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.


Refactoring


  1. Create a folder src / components / HelloWorldPage
  2. Rename App.jsx to HelloWorldPage.jsx , App.css to HelloWorldPage.css
  3. Move the HelloWorldPage.jsx and HelloWorldPage.css files to the src / components / HelloWorldPage folder

 mkdir src/components/HelloWorldPage mv src/components/App.jsx src/components/HelloWorldPage/HelloWorldPage.jsx mv src/components/App.css src/components/HelloWorldPage/HelloWorldPage.css 

  1. Let's make changes to HelloWorldPage.jsx

 --- import './App.css'; +++ import './HelloWorldPage.css'; 

  1. Create an index.js file with the following content

src / components / HelloWorldPage / index.js


 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.


Create App.jsx


  1. Create App folder
  2. Create two files in the App folder: index.js and App.jsx

 mkdir src/components/App 

src / components / App / index.js


 import App from './App'; export default App; 

src / components / App / App.jsx


 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.


This component can be implemented better.

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


src / components / App / 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.


src / components / App / bootstrap.css


 --- @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 .


2. Add a few pages and routing.


2.1. We make two plugs


  1. Create folders src / components / CounterPage and src / components / TimePage
  2. Write the stub code

src / components / CounterPage / index.js


 import CounterPage from './CounterPage'; export default CounterPage; 

src / components / CounterPage / CounterPage.jsx


 import React, { Component } from 'react'; class CounterPage extends Component { render() { return <div>  </div>; } } export default CounterPage; 

src / components / TimePage / index.js


 import TimePage from './TimePage'; export default TimePage; 

src / components / TimePage / TimePage.jsx


 import React, { Component } from 'react'; class TimePage extends Component { render() { return <div>  </div>; } } export default TimePage; 

2.2. Add routing


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:


  1. Define a file with routes . In it, we indicate the correspondence between the URL and the components that should be rendered.
  2. At the server side of the application, the express web server will pass the request URL to the match function from react-router . It will either return renderProps , which we can use to render the content requested by the user, or will report that there are no matches, and then we will return the page with 404 errors.
  3. We will also make changes to the client part of the application so that the react-router library can track URL changes. If the new URL matches one of the configured paths, then client-side JavaScript updates the page content without contacting the server. If the new URL does not match any of the configured paths, then the browser will follow the classic link.

2.2.1. Routes file


src / routes.jsx


 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.


Thus, we have determined the following correspondence


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.


src / components / App / App.jsx


 --- 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; 

2.2.2 Add Routing to the Server Side of the Application


src / server.js


 --- 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


src / client.js


 // 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.


Add links to other pages. This is usually done like this:


 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 

src / components / App / App.jsx


 +++ 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.


2.2.3 Add routing to the client part of the application.


src / client.js


 --- 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!


Intermediate result


We added several pages and successfully configured client and server routing, making sure that they work correctly for all scenarios.


3. Flux and Redux


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.


src / components / CounterPage / Counter.jsx


 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; 

src / components / CounterPage / Counter.css


 .counter-label { display: inline-block; margin-right: 20px; } 

src / components / CounterPage / StateCounter.jsx


 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; 

src / components / CounterPage / CounterPage.jsx


 +++ 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".


Flux

Note: Flux is a concept, not a library. Today, there are many different libraries that implement it.


3.1. Flux basics on the fingers


  1. Components do not contain business logic, but are responsible only for rendering the interface.


  2. 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.


  3. It is forbidden to explicitly change the global state inside the component. To change the global state, components call the special function dispatch . Tracking the progress of this function is anti-pattern, since it violates the first principle of Flux . In practice, the global state will contain all the information you need for your component, for example, the execution status of the API request and errors. This and other information will be promptly and explicitly transmitted to your component using props .

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.


Practical example


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.





Scenario from the user's point of view


  1. The user clicks on the button "Add to cart".
  2. The button is no longer active, its icon is changed to the download indicator.
  3. Server API query in progress.
  4. After the server part has successfully processed the request and returned the answer, a hint appears on top of “The item was successfully added to the basket”, the icon value with the number of goods is increased by one, the amount is recalculated, a new record appears in the block with details of the contents of the basket. The download indicator disappears, and the button itself becomes active again.
  5. In case of an error at step 3, we show the user a message with an error and return the button for adding the product to the cart in its original state.

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".


The same scenario from the point of view of Flux


Note: the "Add to Cart", "Notifications", "Basket" and "Cart Detail" components are subscribed to a global state.


  1. The user clicks on the "Add to cart" button, which leads to the call of the dispatch function.
  2. This function updates the global state: the Add to Cart button receives a new prop 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.
  3. The next step is to make an API request to save information about the added product in the backend.
  4. If successful, we update the global state: the Notifications component receives a new prop 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.
  5. If the API returns an error, the Notifications component will receive the corresponding prop message value and show the user an informational message.
  6. The function last updates the global state by telling the "Add to Cart" button a new prop loading value false . The button will return to its original state.

Code example of such a function


 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.


.


  1. , -,

 <Button onClick={() => dispatch(addItemToCart(3))} /> 

  1. .


  2. , , props .

!


3.2. Redux


: Flux .


Pros:



Minuses:



:


  1. ;
  2. - ;
  3. redux ;
  4. redux ;
  5. redux ;
  6. "", ;
  7. ;
  8. .

: : " ?" " ?". Nothing! , . , . , - , ! , , , , !


3.2.1. redux , react-redux redux-thunk


 npm i --save redux react-redux redux-thunk 

3.2.2. -


3.2.2.1 src/redux , src/redux/actions src/redux/reducers .
3.2.2.2 counterActions.js . , .


src/redux/actions/counterActions.js


 export const INCREMENT_COUNTER = 'INCREMENT_COUNTER'; export function incrementCounter() { return { type: INCREMENT_COUNTER }; } 

3.2.2.3 counterReducer.js . .


src/redux/reducers/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; } } 

?


  1. , 0.
  2. INCREMENT_COUNTER , value 1.
  3. — .

: , redux ( "") @@INIT , .


3.2.3 redux


configureStore.js


src/redux/configureStore


 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)); } 

?


  1. combineReducers , . , , -.
  2. . .
  3. thunk middleware . "" — , dispatch . , .
  4. createStore , "" .

3.2.4. redux


src/server.js


 +++ 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. , , , .


3.2.5. redux


src/client.js


 +++ import { Provider } from 'react-redux'; +++ import configureStore from './redux/configureStore'; +++ const store = configureStore(); const component = ( +++ <Provider store={store}> <Router history={browserHistory}> {routes} </Router> +++ </Provider> ); 

3.2.6. "",


src/components/CounterPage/ReduxCounter.jsx


 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 . :


  1. mapStateToProps , , "" , ;
  2. dispatch ;
  3. , mapStateToProps "" props ReduxCounter .

: connect High Order Components HOCs .


High Order Components

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; } } 

, export default connect(mapStateToProps)(ReduxCounter) ?


  1. connect mapStateToProps .
  2. connect mapStateToProps , . { value: "_____"} , injectedProps .
  3. connect .
  4. ReduxCounter .
  5. props , ( {...this.props} ), injectedProps dispatch

3.2.7. CounterPage


src/components/CounterPage/CounterPage.jsx


 +++ import ReduxCounter from './ReduxCounter'; <StateCounter /> +++ <h3>Redux Counter</h3> +++ <ReduxCounter /> 

3.2.8. We are testing


  1. "+" . .
  2. , . , . Great! Works!

, , !


github — https://github.com/yury-dymov/habr-app/tree/v2 .



  1. .
  2. API.
  3. API.


  1. react-bootstrap
  2. react-router
  3. Flux
  4. redux

Ps , , . Thank you in advance!


')

Source: https://habr.com/ru/post/310284/


All Articles