Redux: Документы: Использование с React Router

Созданный на 27 авг. 2015  ·  61Комментарии  ·  Источник: reduxjs/redux

У людей постоянно складывается впечатление, что мы не поддерживаем React Router, или что для его работы нужно что-то особенное, например, redux-react-router , или даже что он не работает до React 0.14.

Вы можете использовать Redux с React Router 0.13 или 1.0, как сегодня.
(Кстати, это было верно с момента первого выпуска Redux.)

Некоторые функции, такие как путешествия во времени, должны будут подождать поддержки со стороны RR, но это не имеет отношения к основной проблеме. Люди сбиваются с толку, думая, что сегодня они не могут использовать маршрутизацию, что просто неправильно.

В реальном примере , включенном в этот репозиторий, используется React Router. Все, что вам нужно, это обернуть <Router> в <Provider> , точно так же, как вы обернули бы свой компонент верхнего уровня в приложении без маршрутизатора.

Если вы хотите перейти от создателей действий, передайте экземпляр router в качестве параметра создателям действий и вызовите его методы, когда захотите. Если вы хотите прочитать состояние маршрутизатора из хранилища Redux, запустите действие при изменении маршрута и напишите редюсер для его обработки. Вот и все!

react-redux-router — это эксперимент по созданию более естественного API, в котором вы просто отправляете действия и читаете состояние из хранилища, которое автоматически подключается к экземпляру маршрутизатора, но вам не нужно его использовать, или ждать, пока он станет стабильным! Это только эксперимент.

Нам нужно это в документах.

Самый полезный комментарий

Кроме того, как вы относитесь к добавлению примера, который показывает повсеместное использование маршрутизатора React?

Все 61 Комментарий

Первоначально я написал пример рендеринга на стороне сервера, чтобы использовать реагирующий маршрутизатор в универсальном контексте. Было высказано предположение, что использование с реактивным маршрутизатором может стать отдельным документом. Возможно, мы могли бы добавить раздел в документацию?

Кроме того, как вы относитесь к добавлению примера, который показывает повсеместное использование маршрутизатора React?

Я думаю, было бы неплохо изменить пример real-world , сделав его универсальным. Таким образом, вам не нужно ничего создавать с нуля.

Если вы хотите перейти от создателей действий, передайте экземпляр маршрутизатора в качестве параметра создателям действий.

Просто хотел уточнить, это работает только в 0.13, так как 1.0beta не имеет концепции createRouter (пока?), верно?

Вы можете сделать это с помощью 1.0.0-beta3 из ваших компонентов React.

class Thing extends Component {
  static contextTypes = {
    router: PropTypes.object
  }

  handleThing() {
    this.props.actionCreator(this.context.router);
  }
}

@timdorr Безопасно ли использовать this.context ? У меня сложилось впечатление, что он не предназначен для наружного применения.

Да, это просто недокументировано, а не небезопасно. В 0.14 он немного изменился, но не так, чтобы это сломалось. Я полагаю, что это будет задокументировано в какой-то момент в ближайшее время.

@timdorr Означает ли это также, что в 1.0.0-beta3 можно перейти на другой URL-адрес от создателя действия?

Да, если вы передаете экземпляр маршрутизатора создателю действия, вы можете делать с ним все, что хотите, включая переходы.

Я собрал это:

const loginProps = {
  handleLogin: ({email, password}) => store.dispatch(userLogin({email, password})),
};



const routes = (
  <Route path="/" handler={App}>
    <Route path="login" handler={wrapper(Login, loginProps)} />
    <Route handler={authSection}>
      <Route path="" handler={Index} />
      <Route path="users" handler={wrapper(Users, (() => store.dispatch(getUsers())))} />
      <Route path="logout" handler={wrapper(Login, (() => store.dispatch(userLogout())))} />
    </Route>
  </Route>
);

const router = createRouter({
  location: HistoryLocation,
  routes,
});

store.dispatch(receiveRouter({router}));

Warning: Failed Context Types: Required context `store` was not specified in `SmartComponent(TodoApp)`. Check the render method of `Router`.

Что может быть не так?

PS: RR 1.0.0-beta3

@gyzerok Убедитесь, что вы обернули весь () => <Router>stuff</Router> в <Provider> , как это делает пример real-world .

@gaearon да, конечно. Я оборачиваю его не в Provider, а в свой собственный компонент, который по большей части является копипастом вашего Provider. Разница в том, что я не передаю ему магазин, а создаю магазин внутри него.

@gyzerok Трудно сказать, что не так, не видя кода. (И, пожалуйста, создайте отдельную проблему, react-redux — хорошее место.)

Спасибо за пример real-wolrd ! Но как быть с AsyncProps ? Похоже, манипуляции с двойным контекстом не будут работать вместе.

import React from 'react';                                                                                                                                                                                         
import {createStore} from 'redux';                                                                                                                                                                                 
import {Provider} from 'react-redux';                                                                                                                                                                              
import {Router, Route} from 'react-router';                                                                                                                                                                        
import BrowserHistory from 'react-router/lib/BrowserHistory';                                                                                                                                                      
import AsyncProps from 'react-router/lib/experimental/AsyncProps';                                                                                                                                                 

import App from './containers/App';                                                                                                                                                                                
import reducers from './reducers';                                                                                                                                                                                 

const store = createStoreWithMiddleware(reducers);                                                                                                                                                                 
const history = new BrowserHistory();                                                                                                                                                                               

React.render(                                                                                                                                                                                                       
    <Provider store={store}>                                                                                                                                                                                        
        {() =>                                                                                                                                                                                                      
            <Router history={history} createElement={AsyncProps.createElement}>                                                                                                                                     
                <Route component={AsyncProps}>                                                                                                                                                                      
                    <Route path="/" component={App} />                                                                                                                                                              
                </Route>                                                                                                                                                                                            
            </Router>                                                                                                                                                                                               
        }                                                                                                                                                                                                           
    </Provider>,                                                                                                                                                                                                    
    document.body                                                                                                                                                                                                  
);

и App.js

import React from 'react';                                                                                                                                                                                         
import {connect} from 'react-redux';                                                                                                                                                                               

let App = React.createClass({                                                                                                                                                                                      
    statics: {                                                                                                                                                                                                     
        loadProps(params, cb) {                                                                                                                                                                                    
            // have to call this with AsyncProps                                                                                                                                                                   
        }                                                                                                                                                                                                          
    },                                                                                                                                                                                                             
    displayName: 'App',                                                                                                                                                                                            

    render() {                                                                                                                                                                                                     
        return <div children="this is app" />                                                                                                                                                                      
    }                                                                                                                                                                                                              
});                                                                                                                                                                                                                

export default connect(state => state)(App); 

Он будет работать без обёртки connect , но и redux тоже не будет. Кто-нибудь сталкивался с этой проблемой?

Или может быть какой-то другой способ приостановить навигацию до загрузки данных?

Статика - это просто статика. Ставить их можно на что угодно, включая результат connect() .

let App = React.createClass({                                                                                                                                                                                      
    displayName: 'App',                                                                                                                                                                                            

    render() {                                                                                                                                                                                                     
        return <div children="this is app" />                                                                                                                                                                      
    }                                                                                                                                                                                                              
});                                                                                                                                                                                                                

App = connect(state => state)(App); 

App.loadProps = function loadProps(params, cb) {                                                                                                                                                                                    
  // have to call this with AsyncProps                                                                                                                                                                   
}                                                                                                                                                                                                          

export default App; 

@gaearon извините, пример неполный. Я попытался расширить результат connect с отсутствующими статическими реквизитами, но настоящая проблема с контекстом. Дайте мне немного времени, я отправлю весь пример в отдельный репозиторий.

Еще одна вещь, которую я сейчас исследую, — это хранение параметров в хранилище избыточности четким и ненавязчивым способом. Попробовав несколько подходов, я написал:

<Route
  component={OrderDetails}
  path='/orders/:orderId'
  onEnter={({params}) => store.dispatch(setCurrentOrder(params.orderId))} 
/>

поэтому можно ссылаться на параметры из селекторов, как показано ниже:

export const OrderDetails = state => {
  const {order} = state;
  return {
    order: order.details.get(order.currentOrderId),
    orderId: order.currentOrderId,
    isLoading: order.isLoadingDetails,
    error: order.detailsLoadingError
  };
};

Это, вероятно, изменится, как только будет выпущена более стабильная версия react-redux-router .

Хорошие новости: React Router 1.0 RC теперь предоставляет нужные нам хуки.
Посетите https://github.com/acdlite/redux-react-router и сообщите нам, нравится ли вам это прямо сейчас!

@gaearon я обнаружил проблему с react-router и экспериментальным AsyncProps . Обновление реакции решает проблему

@wtfil Полезно знать!

При интеграции с реактивным маршрутизатором я понимаю из этого потока, что мы можем передать экземпляр маршрутизатора создателю действия и вызвать на нем методы. Однако я также понимаю, что разработчики экшенов не должны иметь побочных эффектов в чистом виде. Единственный случай, когда я вижу, что создателям действий разрешено иметь побочные эффекты, — это когда они являются асинхронными действиями, обрабатываемыми промежуточным программным обеспечением. Ожидается ли тогда, что создатели действий, которые выполняют переходы, должны быть асинхронными, а не чистыми функциями?

Ожидается ли тогда, что создатели действий, которые выполняют переходы, должны быть асинхронными, а не чистыми функциями?

Создателям действий разрешено иметь побочные эффекты. Лучше избегать их, когда это возможно, но, конечно, в какой-то момент они вам понадобятся, и, поскольку редукторы в Redux чисты, и у нас нет явного механизма эффектов, как в Elm (см. # 569 для обсуждения), создатели действий - это место для их размещения.

Проверьте редукс-маршрутизатор . Он работает поверх React Router, но позволяет отправлять действия и заботится о синхронизации маршрутизатора.

ждем, когда React Router и Redux Router достигнут версии 1.0...

Ага. Мы добавим рецепт после того, как это произойдет.

Поэтому я следил за этим обсуждением, а также немного думал о том, как маршрутизация в целом вписывается в избыточность (см. https://github.com/rackt/redux/issues/805). Основываясь на некоторых обсуждениях в этой теме и некоторых экспериментах, я нашел подход, который лично я предпочитаю клею react-router/react-redux-router.

По сути, я стараюсь не давать react-router или redux каких-либо знаний друг о друге, а вместо этого соединяю их через собственную реализацию истории. При таком подходе маршрутизация обрабатывается следующим образом:

  1. Действие, редьюсер и ключ в хранилище создаются за route .
  2. Стандартная реализация истории (в этом примере используется предпочтительная createHistory , но вы можете легко использовать createHashHistory или что-то еще) создается и прослушивается. Когда текущее местоположение в браузере изменяется, отправляется действие ROUTE, в конечном итоге помещающее местоположение в хранилище.
  3. Стандартный экземпляр react-router создается со второй настраиваемой реализацией истории, которая уведомляет подписчика (react-router) об изменении ключа route хранилища. Он также определяет createHref и pushState , делегируя их стандартной истории, созданной на шаге 2.

Вот и все. Я думаю, что это обеспечивает довольно четкое разделение обязанностей между redux и react-router и устраняет необходимость в использовании еще одной «клеевой» библиотеки. Я вставляю свой код ниже, мне было бы очень интересно услышать любые отзывы:

// please pay attention to library versions, this strategy is only tested with the indicated versions
import React from 'react'; // v0.13.3
import { Provider } from 'react-redux'; // v3.1.0
import { Router, Route, IndexRoute, Link } from 'react-router'; // v1.0.0-rc3
import { createHistory } from 'history'; // v1.12.3
import { createStore } from 'redux'; // v3.0.2

// define some components
class About extends React.Component {
    render () {
        return (
            <div><h1>About</h1></div>
        )
    }
}
class Home extends React.Component {
    render () {
        return (
            <div>
                <h1>Home</h1>
                <Link to="/about">Go to about</Link>
            </div>
        )
    }
}

// create a standard history object
var history = createHistory();

// set up 'route' action and action creator
const ROUTE = 'ROUTE';
function createRouteAction (location) {
    return {
        type: ROUTE,
        payload: location
    };
}

// set up reducer. here we only define behavior for the route action
function reducer (state = {}, action) {
    if (action.type === ROUTE) {
        return Object.assign({}, state, {
            route: action.payload
        });
    }
    else {
        return state;
        // whatever other logic you need
    }
}

// create store
const store = createStore(reducer);

// this factory returns a history implementation which reads the current state
// from the redux store and delegates push state to a different history.
function createStoreHistory () {
    return {
        listen: function (callback) {
            // subscribe to the redux store. when `route` changes, notify the listener
            const unsubscribe = store.subscribe(function () {
                const route = store.getState().route;
                callback(route);
            });

            return unsubscribe;
        },
        createHref: history.createHref,
        pushState: history.pushState
    }
}

React.render(
    <Provider store={store}>
        {() =>
            <Router history={createStoreHistory()}>
                <Route path="/about" component={About} />
                <Route path="/" component={Home} />
            </Router>
        }
    </Provider>,
    document.getElementById('root') // or whatever
);

// when the url changes, dispatch a route action. this is placed at the bottom so that the first route triggers the initial render
const unlisten = history.listen(function (location) {
    store.dispatch(createRouteAction(location));
});

Кстати, я упоминаю об этом, потому что я думаю, что этот пример может помочь некоторым людям вроде меня, которые боролись с использованием react-router с redux, и поможет прояснить ваше утверждение, что redux и react-router сегодня можно использовать вместе.

@cappslock Мне это нравится, но есть одна мелочь. В вашей реализации изменение маршрута - это действие, это как бы нарушает подход "Действие - это событие, а не команда", что в конечном итоге может привести к некоторым неприятным практикам. Давайте перевернем мышление, потенциально любое действие может привести к побочному эффекту, который изменяет адресную строку (будь то компонент или адресная строка реального браузера....), а побочный эффект приводит к отправке нового действия ( ROUTE_CHANGED ). Это в основном тот же шаблон, что и запуск вызова API.

@tomkis1 спасибо за отзыв. Если я вас правильно понял, это уже так работает. Действие ROUTE отправляется как побочный эффект изменения URL-адреса. Может, имя ROUTE_CHANGED было бы лучше?

Ой! Я только что перепроверил, и вы были правы. Да, ROUTE_CHANGED было бы лучше.

Я тоже так думаю. Я бы вернулся и изменил его, но тогда эти комментарии были бы действительно запутанными :)

Поток такой, чтобы уточнить:

Изменение URL-адреса -> действие ROUTE (или ROUTE_CHANGED ) -> хранилище обновлений редуктора -> история хранения (ранее подписанная на хранилище) уведомляет слушателей -> обновления реакции-маршрутизатора

Я предпочитаю это, потому что мне не нужно ничего, кроме хранилища, управляющего наблюдаемым пользователем состоянием пользовательского интерфейса. Это также кажется предпочтительным для тестирования.

Недостатком этого подхода является то, что URL-адрес не обновляется в ответ на действие ROUTE_CHANGED . Я даже не уверен, желательно ли это, если мы говорим, что не хотим, чтобы действия рассматривались как команды, но я полагаю, что это может быть выполнено либо как побочный эффект создателя действия ROUTE_CHANGED , либо с помощью отдельный подписчик магазина.

Кстати, если это обсуждение выходит за рамки этого вопроса, пожалуйста, дайте мне знать, и я перенесу его.

@cappslock Мне это очень нравится! Я определенно не думаю, что проблема в том, что отправка ROUTE_CHANGED не меняет маршрут. Обработка действия как события, а не триггера, кажется мне чище/более понятным (поскольку оно отвечает на взаимодействие с пользователем; вы не ожидаете, что действие BUTTON_CLICKED фактически вызовет нажатие кнопки). Однако есть одна часть вашего кода, которую я не понимаю. Можете ли вы уточнить этот момент для меня?

это размещено внизу, так что первый маршрут запускает начальный рендеринг

@elliotdickison Спасибо! Я попытаюсь прояснить это, но, пожалуйста, отнеситесь к тому, что я говорю, с недоверием, так как это основано на пробах, ошибках и предположениях, а не на глубоком анализе. На данный момент это скорее доказательство концепции/наброска.

Когда я поместил этот код над экземпляром ReactRouter , компонент, соответствующий маршруту / , не отобразился. Маршрутизатор по-прежнему будет работать, если действия будут отправляться вручную или состояние будет передаваться вручную, поэтому я решил, что это проблема жизненного цикла с историей. Перемещение ниже экземпляра ReactRouter решило эту проблему. Я подозреваю, что библиотека истории откладывает уведомление о начальном маршруте, пока не появится хотя бы один подписчик. Если этот подписчик настроен до ReactRouter, ни одно из уведомлений не доходит до него.

Пытаясь объяснить это, я понимаю, что немного не понимаю; Я посмотрю на это больше и постараюсь дать лучший ответ.

Недостатком этого подхода является то, что URL-адрес не обновляется в ответ на действие ROUTE_CHANGED. Я даже не уверен, желательно ли это, если мы говорим, что не хотим, чтобы действия рассматривались как команды, но я полагаю, что это может быть выполнено либо как побочный эффект создателя действия ROUTE_CHANGED, либо отдельным подписчиком хранилища.

Я бы сказал, что это желательно, ROUTE_CHANGED обязательно должен быть запущен внешним источником (например, onhashchange...) Изменение URL-адреса IMO должно привести к ROUTE_CHANGED , а не наоборот.

Я вроде согласен. Я подумал, что было бы неплохо просто иметь что-то, подписывающееся на магазин и синхронизирующее URL-адрес на случай, если отправленное действие ROUTE_CHANGED возникло из кода, а не из фактического события истории, но вы могли бы утверждать, что этот случай представляет собой ошибка программирования.

@cappslock ваш подход действительно очень хорош. Не могли бы вы сделать об этом пост в блоге?

@vojtaranta Просто загляните на https://github.com/rackt/redux/issues/805 . Думаю, это послужило источником вдохновения для реализации.

@vojtaranta Спасибо! У меня нет блога, поэтому почти вся информация, которая у меня есть, находится в этой теме и #805. О чем конкретно вы хотели узнать?

1.0 вышла.

Пришло время:

  • Пример маршрутизации портов для его использования
  • Добавить рецепт «Использование с маршрутизатором» на основе примера real-world

:хлопок:

@gaearon , можете ли вы сослаться на эту проблему, когда пример _Usage with Router_ находится в PR? Многие люди, которых я знаю (включая меня), ищут разъяснений о том, как эти двое хорошо сочетаются друг с другом.

Да, конечно. На этом вопрос будет закрыт. :-)

Может быть, теперь стоит рассмотреть redux-simple-router ?

редукс-простой-маршрутизатор +1

Я только что преобразовал универсальный пример + react-router (+redux-simple-router)
https://github.com/eriknyk/редукс-универсальное-приложение

привет всем, какой вывод из этого обсуждения? я вижу, что документы не обновлены для использования с реактивным маршрутизатором
копия @gaearon

@gaearon после того, как я привязал свое реагирующее приложение к избыточности, я просто использую состояние для управления отображением/скрытием моих компонентов. Поэтому я не думаю, что первоначальная роль «маршрутизатора», такая как RR, теперь подходит для моего приложения.
Единственное, что, я думаю, нужно сделать «новому маршрутизатору», это сопоставить URL-адрес с состоянием (через действия?) И также переназначить состояние обратно на URL-адрес.
Если мы позволим URL-адресу решать, как отображать компонент приложения (возможно, некоторые из них), это означает, что у нас есть два источника состояния, один — URL-адрес, другой — хранилище редуксов, что усложнит задачу...
Что вы скажете об этом? мы должны просто позволить адресной строке быть еще одним компонентом нашего приложения.

Спасибо

Я официально обязуюсь написать этот документ после того, как мы отправим https://github.com/rackt/react-router-redux/pull/259. Это будет благословенный способ связать React Router и Redux. В документе мы сначала покажем, как использовать их без этого пакета, и постепенно представим два удобства, которые предоставляет пакет: промежуточное ПО и перемещение источника достоверной информации о маршрутизации в хранилище. Оба являются необязательными, поэтому мы обязательно объясним, в каком случае вы хотите их использовать, и что они дают вам по сравнению с обычным RR.

Вот мысль для размышления: если что-то, что вы объясняете о промежуточном программном обеспечении, связанном с маршрутизацией и историей, также может стать конкретным фрагментом реалистичного приложения в рамках общего объяснения http://rackt.org/redux/docs/advanced/Middleware.html ( например, в примерах в конце)

@gaearon есть прогресс в документе React Router / следующих шагах? Я читаю документацию Redux и мне она нравится, но эти поддельные ссылки меня расстраивают :(

Я только начал переписывать документы по реагирующим маршрутизаторам в своем личном репозитории, и я также планирую добавить туда раздел редукции. Я мог бы что-то сделать в ближайшее время, зависит от моего свободного времени. Я буду держать вас в курсе. https://github.com/knowbody/react-router-docs

Честно говоря, вам не нужно ничего на стороне Redux, чтобы заставить работать реактивный маршрутизатор. Но да, мы должны это сделать.

Обратите внимание: React Router 3.0 будет лучше работать с оптимизацией React Redux connect() , а новый HOC withRouter() означает, что вам не нужно напрямую использовать контекст.

https://twitter.com/dan_abramov/status/729768048417251328

@gaearon , @timdorr , не могли бы вы прояснить компромисс между передачей экземпляра маршрутизатора в качестве аргумента создателям действий и прямым импортом истории браузера в создателей действий (как предлагается здесь https://github.com/reactjs/react-router/blob /master/docs/guides/NavigatingOutsideOfComponents.md?

router просто оборачивает ваш экземпляр истории некоторыми дополнительными элементами, но это те же самые методы push и replace между двумя экземплярами.

Спасибо, @timdorr.

Я предполагаю, что тогда возникает вопрос, зачем нам нужна композиция withRouter() , если router в основном обертывает синглтон истории (это синглтон, верно)?

Это сделано для того, чтобы разрешить более слабую связь между компонентом и экземпляром истории (т. е. предотвратить прямой доступ компонента к объекту-одиночке)? Если да, то не будет ли применяться та же логика при доступе к экземпляру истории из создателя действия?

Да, и если вы предоставляете свой собственный экземпляр истории и не хотите (или не можете) создавать свой собственный одноэлементный модуль (что может сбивать с толку, если вы не очень хорошо знакомы с модульными системами JS). Если вы хотите сделать это самостоятельно, вы можете следовать нашему образцу.

Я не уверен, стоит ли документировать withRouter о том, как его можно использовать для нескольких компонентов более высокого порядка. Я все еще пытаюсь найти лучший способ избежать этого:
connect(mapStateToProps, mapDispatchToProps)(withRouter(withAnalytics(withLanguage(TestForm)))); .

Я также мог бы использовать что-то вроде compose ?

const enhance = compose(
  connect(mapStateToProps, mapDispatchToProps),
  withRouter,
  withAnalytics,
  withLanguage
);

export default enhance(TestForm);

Тем не менее, в моем случае использования контекст будет иметь зарегистрированного пользователя, текущий язык, информацию о теме и аналитику, что усложняет задачу с большим количеством контекста и множеством связанных компонентов.

Другая идея состоит в том, чтобы дублировать логику withRouter и connect в одном пространстве имен контекста: withAppContext() => props.app = { user, lang, theme, analytics, router, connect? } ?

Будет ли это полезно для документации или примера использования withRouter с connect ?

@gaearon есть ли какие-либо обновления для этого сейчас, когда React Router 3.0.0 и ваши новые видео Egghead уже давно вышли, а эта ветка открыта уже год?

Совершено в № 1929

Была ли эта страница полезной?
0 / 5 - 0 рейтинги