Powiązanie React, Redux i Router
Router jest bardzo wygodnym rozwiązaniem w tworzeniu nawigacji na stronie. Jednak jego użyciem jest związane są pewne problemy - opisane poniżej. Do ich zaprezentowania stwórzmy prostą aplikację z magazynem Redux przechowującym stan licznika i dostarczającym dwie operacje: zwiększenia licznika i jego ustawienia:
function stanPoczatkowy() {
return {
licznik : 0
}
}
const ACTION_USTAW = 'ACTION_USTAW';
const ACTION_ZWIEKSZ = 'ACTION_ZWIEKSZ';
export function zwieksz() {
return { type: ACTION_ZWIEKSZ }
}
export function ustaw(n) {
return {
type: ACTION_USTAW,
licznik : n
}
}
const naszReducer = (state, action) => {
if (action.type === ACTION_ZWIEKSZ) {
let nowyLicznik=state.licznik+1;
return( {licznik:nowyLicznik} );
} else if (action.type === ACTION_USTAW) {
let nowyLicznik=Number(action.licznik);
return( {licznik:nowyLicznik} );
} else return state;
}
Do jego przetestowania wystarczy komponent:
class Licznik extends Component {
render = () => (
<button onClick={this.props.zwieksz}>
Licznik = { this.props.reduxStore.licznik }
</button>
);
}
Wpiszemy w naszej aplikacji <Licznik />
i możemy testować zwiększanie poprzez klikanie w przycisk. Załóżmy jednak, że chcemy z menu ustawiać ten przycisk od razu na 5. Dodamy do naszego komponentu własność N. Nie jest ona wyświetlana, ale powinna zmienić stan. Można to zrobić w funkcji componentDidMount
(cykl życia):
componentDidMount = () => {
if (this.props.N) this.props.ustaw(this.props.N);
}
Tworzymy w naszej aplikacji Router. Ponieważ nie możemy podać w nim komponentu z parametrami (własnościami) - tworzymy komponent pośredniczący Licznik5:
const Licznik5 = (props) => (<Licznik N={5} />)
class App extends Component {
render() {
return(
<Router>
<div>
[<Link to='/'>Licznik</Link>]
[<Link to='/licznik5'>Licznik5</Link>]
<div className="container">
<Route exact path="/" component={Licznik} />
<Route exact path="/licznik5" component={Licznik5} />
</div>
</div>
</Router>
)}
}
Niezależnie od tego, czy wpiszemy w przeglądarce adres /licznik5
(http://localhost:3000/licznik5
), czy też klikniemy w link "Licznik 5" - uzyskamy ten sam efekt - wyświetlony komponen Licznik z wartością początkową licznika równą 5.
Gdy zamiast własności N użyjemy paremetru routera, możemy ustawiać licznik z URL! Dodajmy zatem elementy routingu:
[<Link to='/licznik/5'>Licznik/5</Link>]
<Route exact path="/licznik/:N" component={Licznik} />
W naszym komponencie aby uwzględnić obie możliwości (własność i parametr routera) zmieniamy nieco componentDidMount:
componentDidMount = () => {
if (this.props.N) this.props.ustaw(this.props.N);
if (this.props.match)
this.props.ustaw(this.props.match.params.N);
}
Możemy wybrać z menu którąś z opcji, lub wstawić do URL wartość licznika (na przykład: http://localhost:3000/licznik/10.
Problem przenoszenia parametrów routera
Co jednak się stanie jeśli w powyższym przykładzie chcemy komponent Licznik umieścić w innym komponencie, ale przekazać parametr z routera?
Na przykład tak:
const LicznikN = (props) => (<div>Licznik z opisem: <Licznik /></div>)
Zmieniamy odpowiednią trasę:
<Route exact path="/licznik/:N" component={LicznikN} />
Jednak po wybraniu licznik/10 wcale nie uzyskamy parametru startowego. Wartość props.match
nie jest przekazywana do potomków. Możemy jednak tą własność "wstrzyknąć" przy pomocy funkcji withRoute (z pakietu react-router-dom):
Licznik=withRouter(Licznik)
Funkcja withRoute
działa podobnie jak connect z Reduxa - wstrzykiwuje własności do props (tu: match z routingu). Te obie fukcje możemy łaczyć razem:
Licznik = connect(
mapStateToProps,
mapDispatchToProps
)(withRouter(Licznik));
Teraz licznik będzie działał zgodnie z oczekiwaniami.
Zobacz przykład [037]
Linkowanie, Zarządzanie historią
Funkcja withRouter
przepisuje dane z kontekstu (context) do props (podobnie jak connect dla Redux). Gdy używamy Routera - wśród tych danych jest nie tylko match
, ale także location
(wybrany link) i history
(cała ścieżka). Pod warunkiem, że pakiet history zostanie zainstalowany:
yarn add history
Komponent history pozwala na przykład na wyświetlenie wybranej ścieżki:
import { withRouter } from 'react-router-dom';
let TestPage = (props) => {
// console.log(props.history.location.pathname);
return (
<div className="container">
{props.history.location.pathname}
</div>
);
}
TestPage = withRouter(TestPage);
Więcej informacji:
Problem synchronizacji
W naszym przykładzie z licznikiem, możemy komponent Licznik wyświetlić na kilka sposobów - samodzielnie albo wewnątrz innego komponentu. Można też podać stan początkowy z URL. Stan aplikacji (niekoniecznie tego komponentu) może się różnić w zależności od tego - którą drogą do niego dotarliśmy. A może chcemy, by wyglądało to identycznie - niezależnie od tego czy zwiększamy licznik klikając, czy podając wartość URL? Pomocnym w tym jest pakiet connected-react-router
. Zapewnia on możliwość synchronizacji Routera z React i Redux. Jego instalacja nie jest banalna:
1) instalujemy history
i connected-react-router :
yarn add history connected-react-router
2) Dodajemy do store Redux'a elementy Middleware związane z obsługą history (moduł magazyn.js):
import { applyMiddleware, compose } from 'redux'
import { createBrowserHistory } from 'history'
import { connectRouter, routerMiddleware } from 'connected-react-router'
.....
.....
export const history = createBrowserHistory()
export const store = createStore(
connectRouter(history)(naszReducer),
stanPoczatkowy(),
applyMiddleware(
routerMiddleware(history),
)
)
3) W funkcji ReactDOM.render (moduł index.js) dostarczamy nie tylko magazyn Reduxa (Provider) ale też obiet history (utworzony wyżej w magazyn.js):
import { store, history } from './magazyn';
import { Provider } from 'react-redux'
import { ConnectedRouter } from 'connected-react-router'
import App from './App';
ReactDOM.render(
<Provider store={store}>
<ConnectedRouter history={history}>
< App />
</ConnectedRouter>
</Provider>,
document.getElementById('root'));
Możemy wreszcie zacząć używać nowych możliwości. Na przykład history.push powoduje zmianę adresu URL:
class Licznik extends Component {
componentDidMount = () => {
if (this.props.N) this.props.ustaw(this.props.N);
if (this.props.match) {
if (this.props.match.params.N)
this.props.ustaw(this.props.match.params.N);
}
this.props.history.push('/licznik/'+this.props.reduxStore.licznik)
}
klik = () => {
let N = this.props.reduxStore.licznik+1;
this.props.zwieksz();
this.props.history.push('/licznik/'+N);
// nie odwołuje się wprost do this.props.reduxStore.licznik
// bo mógł się jeszcze nie zmienić
}
render = () => (
<button onClick={this.klik}>
Licznik = { this.props.reduxStore.licznik }
</button>
);
}
Jak widać - wprowadzono funkcję klik, która nie tylko powoduje zmiany store (zwieksz), ale też url (push). Zobacz przykład [038]