React Router

Лекция 31

React Router

React Router - Install


npm install --save react-router-dom
    

<script
  src="https://unpkg.com/react-router-dom/umd/react-router-dom.min.js"
></script>
    

React Router - Key Components

  • BrowserRouter / HashRouter
  • Route
  • Link

React Router - Quick Start


import React from 'react';

import {
  HashRouter as Router,
  Route,
  Link
} from 'react-router-dom';
    

React Router - Quick Start


const Home = () => (
  <div>
    <h2>Home</h2>
  </div>
);
    

React Router - Quick Start


const About = () => (
  <div>
    <h2>About</h2>
  </div>
);
    

React Router - Quick Start


const frameworks = [
  {
    name:'angular',
    desc: 'Superheroic JavaScript MVW Framework'
  },
  {
    name:'ember',
    desc: 'A framework for creating ambitious web applications'
  },
  {
    name:'react',
    desc: 'A JavaScript library for building user interfaces'
  }
];
    

React Router - Quick Start


const Framework = (props) => {
  const framework = frameworks
    .find(x =>
      x.name === props.match.params.name
    );

  return (
    <div>
      <h3>{framework.name}</h3>
      <p>{framework.desc}</p>
    </div>);
};
    

React Router - Quick Start


const Frameworks = (props) => (
  <div>
    <h2>Frameworks</h2>

    {frameworks.map(x =>
      <div key={x.name}>
        <Link
            to={`${props.match.url}/${x.name}`}>
          {x.name}
        <Link>
      </div>
    )}

    <hr/>
    ...
);
    

React Router - Quick Start


const Frameworks = (props) => (
  <div>
    ...

    <Route
      path={`${props.match.url}/:name`}
      component={Framework}
    />
    <Route
      exact
      path={props.match.url}
      render={() => (
        <h3>Please select a framework.</h3>
      )}
    />
  </div>
);
    

React Router - Quick Start


const App = () => (
  <HashRouter>
    <div>
      <ul>
        <li><Link to="/">Home</Link></li>
        <li><Link to="/about">About</Link></li>
        <li><Link to="/frameworks">Frameworks</Link></li>
      </ul>

      <hr/>

      <Route exact path="/" component={Home}/>
      <Route path="/about" component={About}/>
      <Route path="/frameworks" component={Frameworks} />
    </div>
  </HashRouter>
);
    

React Router - Quick Start


ReactDOM.render(
  <App/>,
  document.getElementById('app')
);
    

React Router - Redux


function humans(state = [], action) {
  switch (action.type) {
    case 'HUMANS_ADD':
      return [
        ...state,
        { name: action.name }
      ];
  }

  return state;
}
    

React Router - Redux


function robots(state = [], action) {
  switch (action.type) {
    case 'ROBOTS_ADD':
      return [
        ...state,
        {
          name: action.name,
          manufacturer: action.manufacturer
        }
      ];
  }

  return state;
}
    

React Router - Redux


const reducer = Redux.combineReducers({
  humans,
  robots
});

const store = Redux.createStore(reducer, {
  humans: [
    { name: 'Leela' },
    { name: 'Fry' }
  ],
  robots: [
    { name: 'Bender', manufacturer: 'MomCorp'}
  ]
});
    

React Router - Redux


const App = () => (
  <HashRouter>
    <div>
      <Link to="/">
        Home
      </Link>
      {' '}
      <Link to="/humans">
        Humans
      <Link>
      {' '}
      <Link to="/robots">
        Robots
      </Link>
      ...
    </div>
  </HashRouter>
);
    

React Router - Redux


const App = () => (
  <HashRouter>
    <div>
      ...
      <Route exact path="/"
        component={Home}/>

      <Route path="/humans"
        component={HumansContainer}/>

      <Route path="/robots"
        component={RobotsContainer}/>
      ...
    </div>
  </HashRouter>
);
    

React Router - Redux


const App = () => (
  <HashRouter>
    <div>
      ...
      <br/>

      <StatsContainer />
    </div>
  </HashRouter>
);
    

React Router - Redux


const Home = () => (
  <h2>Welcome to Futurama!</h2>
);
    

React Router - Redux


const HumansContainer = ReactRedux.connect(
  humansMapStateToProps,
  humansMapDispatchToProps
)(Humans);
    

React Router - Redux


const humansMapStateToProps = (state) => {
  return {
    list: state.humans,
  };
};

const humansMapDispatchToProps = (dispatch) => {
  return {
    add: (human) => {
      dispatch({
        type: 'HUMANS_ADD',
        name: human.name
      });
    }
  };
};
    

React Router - Redux


const Humans = (props) => {
  const items = props.list.map(item => (
    <div key={item.name}>{item.name}</div>
  ));

  return (
    <div>
      <h2>Humans</h2>
      {items}
      <br/>
      <AddItem
        fields={['name']}
        add={props.add}
      />
    </div>
  );
};
    

React Router - Redux


const RobotsContainer = ReactRedux.connect(
  robotsMapStateToProps,
  robotsMapDispatchToProps
)(Robots);
    

React Router - Redux


const robotsMapStateToProps = (state) => {
  return {
    list: state.robots,
  };
};

const robotsMapDispatchToProps = (dispatch) => {
  return {
    add: (robot) => {
      dispatch({
        type: 'ROBOTS_ADD',
        name: robot.name,
        manufacturer: robot.manufacturer
      });
    }
  };
};
    

React Router - Redux


const Robots = (props) => {
  const items = props.list.map(item => (
    <div key={item.name}>
      {item.name} [{item.manufacturer}]
    </div>
  ));

  return (
    <div>
      <h2>Robots</h2>
      {items}
      <br/>
      <AddItem fields={['name', 'manufacturer']}
        add={props.add} />
    </div>
  );
};
    

React Router - Redux


class AddItem extends React.Component {
  ...
}
    

React Router - Redux


constructor(props) {
  super(props);

  this.state = {};

  props.fields.forEach(x => this.state[x] = '');

  this.__change = this.__change.bind(this);
  this.__add = this.__add.bind(this);
}
    

React Router - Redux


render() {
  const inputs = this.props.fields.map(x => {
    const handler = (event) => {
      this.__change(x, event);
    };

    return <input key={x}
      value={this.state[x]}
      placeholder={x} onChange={handler} />;
  });

  return (
    <div>
      {inputs}
      <button onClick={this.__add}>Add</button>
    </div>
  );
}
    

React Router - Redux


__change(field, event) {
  const state = {};

  state[field] = event.target.value;

  this.setState(state);
}
    

React Router - Redux


__add() {
  this.props.add(this.state);

  const state = {};

  this.props.fields.forEach(x => state[x] = '');

  this.setState(state);
}
    

React Router - Redux


const Stats = (props) => (
  <div>Humans to kill: <b>{props.toKill}</b></div>
);
    

React Router - Redux


const statsMapStateToProps = (state) => {
  return {
    toKill: state.humans.length,
  };
};

const StatsContainer = ReactRedux.connect(
  statsMapStateToProps
)(Stats);
    

React Router - Redux


ReactDOM.render(
  <ReactRedux.Provider store={store}>
    <App/>
  </ReactRedux.Provider>,
  document.getElementById('app')
);
    

React Router - Browser Router


const App = (props) => (
  <BrowserRouter>
    ...
  </BrowserRouter>
);
    

React Router - Browser Router


const express = require('express');
const path = require('path');

const app = express();

app.use(express.static('public'));

app.get('/api', ...);

app.get('/*', (req, res) => {
  res.sendFile(
    path.join(__dirname, 'public/index.html')
  );
});

app.listen(4000, () => { console.log('Running'); });
    

React Router - Browser Router


basename: string

<BrowserRouter basename="/calendar"/>
<Link to="/today"/>
// renders <a href="/calendar/today">
    

React Router - Browser Router


getUserConfirmation: func

// this is the default behavior
const getConfirmation = (message, callback) => {
  const allowTransition = window.confirm(message)

  callback(allowTransition)
}

<BrowserRouter
  getUserConfirmation={getConfirmation}
/>
    

React Router - Browser Router


forceRefresh: bool

const supportsHistory
  = 'pushState' in window.history;

<BrowserRouter
  forceRefresh={!supportsHistory}
/>
    

React Router - Browser Router


keyLength: number

// unique key in location stack
// defaults to 6
<BrowserRouter
  keyLength={12}
/>
    

React Router - Hash Router


basename: string
getUserConfirmation: func
    

React Router - Hash Router


hashType: string

// defaults to "slash"
"slash" - #/sunshine/lollipops
"noslash" - #sunshine/lollipops
"hashbang" - #!/sunshine/lollipops
    

React Router - Link


to: string

<Link to="/courses"/>
    

React Router - Link


to: object

<Link to={{
  pathname: '/courses',
  search: '?sort=name',
  hash: '#the-hash',
  state: { fromDashboard: true }
}}/>
    

React Router - Link


replace: bool

// replace current entity in history stack
<Link to="/courses" replace />
    

React Router - NavLink


activeClassName: string

<NavLink
  to="/faq"
  activeClassName="selected"
>FAQs</NavLink>
    

React Router - NavLink


activeStyle: object

<NavLink
  to="/faq"
  activeStyle={{
    fontWeight: 'bold',
    color: 'red'
   }}
>FAQs</NavLink>
    

React Router - NavLink


exact: bool

// active class/style only if exact match
<NavLink
  to="/faq"
  exact
  activeStyle={{
    fontWeight: 'bold',
    color: 'red'
   }}
>FAQs</NavLink>
    

React Router - NavLink


isActive: func

const oddEvent = (match, location) => {
  if (!match) {
    return false
  }
  const eventID = parseInt(match.params.eventID)
  return !isNaN(eventID) && eventID % 2 === 1
}

<NavLink
  to="/events/123"
  isActive={oddEvent}
>Event 123</NavLink>
    

React Router - Redirect


import { Route, Redirect } from 'react-router'

<Route exact path="/" render={() => (
  loggedIn ? (
    <Redirect to="/dashboard"/>
  ) : (
    <PublicHomePage/>
  )
)}/>
    

React Router - Redirect


to: string

<Redirect to="/somewhere/else"/>
    

React Router - Redirect


to: object

<Redirect to={{
  pathname: '/login',
  search: '?utm=your+face',
  state: { referrer: currentLocation }
}}/>
    

React Router - Redirect


push: bool

// add new entry to history, not replace
<Redirect push to="/somewhere/else"/>
    

React Router - Route


import {
  BrowserRouter as Router,
  Route
} from 'react-router-dom'

<Router>
  <div>
    <Route exact path="/" component={Home}/>
    <Route path="/news" component={NewsFeed}/>
  </div>
</Router>
    

React Router - Route


// route '/'
<div>
  <Home/>
  <!-- react-empty: 2 -->
</div>
    

React Router - Route


// route '/news'
<div>
  <!-- react-empty: 1 -->
  <NewsFeed/>
</div>
    

React Router - Route


// 3 render ways:
<Route component>
<Route render>
<Route children>

// all render ways will receive 3 props:
match
location
history
    

React Router - Route


// uses React.createElement
<Route path="/user/:username" component={User}/>

const User = ({ match }) => {
  return <h1>Hello {match.params.username}!</h1>
}
    

React Router - Route


// convenient inline rendering
<Route path="/home"
  render={() => <div>Home</div>}
/>
    

React Router - Route


// render even if URL is not match
<ul>
  <ListItemLink to="/somewhere"/>
  <ListItemLink to="/somewhere-else"/>
</ul>

const ListItemLink = ({ to, ...rest }) => (
  <Route path={to} children={({ match }) => (
    <li className={match ? 'active' : ''}>
      <Link to={to} {...rest}/>
    </li>
  )}/>
)
    

React Router - Route


path: string

// routes without a path always match
<Route path="/users/:id" component={User}/>
    

React Router - Route


exact: bool

<Route exact path="/one" component={About}/>
    
path location.pathname exact matches?
/one /one/two true no
/one /one/two false yes

React Router - Route


strict: bool

<Route strict path="/one/" component={About}/>
    
path location.pathname matches?
/one/ /one no
/one/ /one/ yes
/one/ /one/two yes

React Router - Switch


<Route path="/about" component={About}/>
<Route path="/about" component={About}/>
<Route path="/:user" component={User}/>
<Route component={NoMatch}/>
    

React Router - Switch


<Switch>
  <Route exact path="/" component={Home}/>
  <Route path="/about" component={About}/>
  <Route path="/:user" component={User}/>
  <Route component={NoMatch}/>
</Switch>
    

React Router - Switch


<Switch>
  <Route exact path="/" component={Home}/>

  <Route path="/users" component={Users}/>
  <Redirect from="/accounts" to="/users"/>

  <Route component={NoMatch}/>
</Switch>
    

React Router - history (prop)


// The number of entries in the history stack
length - (number)
    

React Router - history (prop)


// The current action (PUSH, REPLACE, or POP)
action - (string)
    

React Router - history (prop)


// The current location
location - (object)
    

React Router - history (prop)


// The path of the URL
location.pathname - (string)
    

React Router - history (prop)


// The URL query string
location.search - (string)
    

React Router - history (prop)


// The URL hash fragment
location.hash - (string)
    

React Router - history (prop)


// location-specific state that was provided
// e.g. push(path, state)
// only available in browser and memory history.
location.state - (string)
    

React Router - history (prop)


// Pushes a new entry onto the history stack
push(path, [state]) - (function)
    

React Router - history (prop)


// Replaces the current entry on the history stack
replace(path, [state]) - (function)
    

React Router - history (prop)


// Moves the pointer in the history stack by n
go(n) - (function)
    

React Router - history (prop)


// Equivalent to go(-1)
goBack() - (function)
    

React Router - history (prop)


// Equivalent to go(1)
goForward() - (function)
    

React Router - history (prop)


// Prevents navigation
block(prompt) - (function)
    

React Router - match (prop)


// Key/value pairs parsed from the URL
// corresponding to the dynamic
// segments of the path
params - (object)
    

React Router - match (prop)


// true if the entire URL was matched
isExact - (boolean)
    

React Router - match (prop)


// The path pattern used to match.
path - (string)
    

React Router - match (prop)


// The matched portion of the URL
url - (string)
    

React Router - match (prop)


matchPath('/users/123', {
  path: '/users/:id',
  exact: true,
  strict: false
})

=>

{ isExact: true,
  params: { id: "123" },
  path: "/users/:id",
  url: "/users/123" }
    

React Router - withRouter


withRouter(connect(...)(MyComponent)
    

React Router - withRouter


class UpdateBlocker extends React.PureComponent {
  render() {
    return this.props.children
  }
}
    

React Router - withRouter


// location = { pathname: '/about' }
<UpdateBlocker>

  <NavLink to='/about'>About</NavLink>
  // <a href='/about' class='active'>About</a>

  <NavLink to='/faq'>F.A.Q.</NavLink>
  // <a href='/faq'>F.A.Q.</a>

</UpdateBlocker>
    

React Router - withRouter


// location = { pathname: '/faq' }
<UpdateBlocker>

  // the links will not re-render,
  // so they retain their previous attributes

  <NavLink to='/about'>About</NavLink>
  // <a href='/about' class='active'>About</a>

  <NavLink to='/faq'>F.A.Q.</NavLink>
  // <a href='/faq'>F.A.Q.</a>

</UpdateBlocker>
    

React Router - withRouter


const BlockAvoider = withRouter(UpdateBlocker)
    

React Router - MemoryRouter


import { MemoryRouter } from 'react-router'

/* for tests & non-browser envs */
<MemoryRouter>
  <App/>
</MemoryRouter>
    

React Router - Deep Redux Integration


npm i react-router-redux@next
npm i history
    

React Router - Deep Redux Integration


// store.js
import {
  createStore,
  applyMiddleware,
  compose } from 'redux';

import createSagaMiddleware
  from 'redux-saga';

import createHistory
  from 'history/createHashHistory';

import { routerMiddleware }
  from 'react-router-redux';
    

React Router - Deep Redux Integration


// store.js
import reducer from './reducers';
import sagas from './sagas';
import initialState from './initial-state';
    

React Router - Deep Redux Integration


// store.js
const history = createHistory();
const sagaMiddleware = createSagaMiddleware();

let composeEnhancers = compose;

if (window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__) {
    composeEnhancers = window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__;
}

const store = createStore(
    reducer,
    initialState,
    composeEnhancers(
      applyMiddleware(sagaMiddleware),
      applyMiddleware(routerMiddleware(history))
    )
);
    

React Router - Deep Redux Integration


// store.js
sagaMiddleware.run(sagas);

store.history = history;

export default store;
    

React Router - Deep Redux Integration


// index.js
import React from 'react';
import ReactDOM from 'react-dom';
import { ConnectedRouter } from 'react-router-redux';
import { Provider } from 'react-redux';
import Store from './store';
import App from './containers/app';

const Root = ({ store }) => (
  <Provider store={store}>
    <ConnectedRouter history={store.history}>
      <App />
    </ConnectedRouter>
  </Provider>
);

ReactDOM.render(<Root store={Store}/>, document.getElementById('app'));
    

React Router - Deep Redux Integration


// dispatchers/goto.js
import { push } from 'react-router-redux';

export default (Actions, route) =>
  dispatch =>
    name => {
      dispatch( Actions.select({ name }) );
      dispatch( push(`${route}/${name}`) );
};
    

React Router - Deep Redux Integration


// sagas/admins/create.js
import { put, ... } from 'redux-saga/effects';
import { push } from 'react-router-redux';

export default function * () {
  ...

  yield put(push(`/dashboard/admins/${login}`));

  ...
}
    

React Router - Deep Redux Integration


// sagas/vfs/create.js
import { select, ... } from 'redux-saga/effects';

export default rootRoute => function * () {
  ...
  const { router: { location: { pathname } } }
    = yield select();

  if (pathname !== rootRoute
      && pathname !== rootRoute + '/'
  ) {
    name = `${pathname
        .replace(rootRoute + '/', '')}
      /${name}`;
  }
  ...
}