Как обновить хранилище Redux после запроса Apollo GraphQL
Я собираю список данных с graphql
HOC, предоставляемый реагировать на apollo. Например:.
const fetchList = graphql(
dataListQuery, {
options: ({ listId }) => ({
variables: {
listId,
},
}),
props: ({ data: { loading, dataList } }) => {
return {
loading,
list: dataList,
};
}
}
);
Я показываю список в управляемой группе переключателей, и мне нужно выбрать один из элементов по умолчанию. Элемент id
выбранного элемента хранится в хранилище Redux.
Итак, вопрос в том, как обновить хранилище Redux (т.е. установить selectedItem
) после успешного завершения запроса?
Некоторые варианты, которые пришли мне на ум:
Вариант 1
Должен ли я слушать действия APOLLO_QUERY_RESULT
в моем редукторе Redux? Но это неловко, потому что тогда мне нужно будет слушать как APOLLO_QUERY_RESULT
, так и APOLLO_QUERY_RESULT_CLIENT
, если запрос уже выполнялся раньше. А также operationName
prop присутствует только в действии APOLLO_QUERY_RESULT
, а не в действии APOLLO_QUERY_RESULT_CLIENT
. Поэтому мне нужно было бы проанализировать каждое действие APOLLO_QUERY_RESULT_CLIENT
, чтобы знать, откуда оно взялось. Не существует ли простой и прямой способ идентифицировать действия с результатами запроса?
Вариант 2
Мне нужно отправить отдельное действие вроде SELECT_LIST_ITEM
в componentWillReceiveProps
например (используя recompose):
const enhance = compose(
connect(
function mapStateToProps(state) {
return {
selectedItem: getSelectedItem(state),
};
}, {
selectItem, // action creator
}
),
graphql(
dataListQuery, {
options: ({ listId }) => ({
variables: {
listId,
},
}),
props: ({ data: { loading, dataList } }) => ({
loading,
items: dataList,
}),
}
),
lifecycle({
componentWillReceiveProps(nextProps) {
const {
loading,
items,
selectedItem,
selectItem,
} = nextProps;
if (!selectedItem && !loading && items && items.length) {
selectItem(items[items.length - 1].id);
}
}
})
);
Вариант 3
Должен ли я использовать клиента Apollo напрямую, введя его с помощью withApollo
, а затем отправьте мое действие с помощью client.query(...).then(result => { /* some logic */ selectItem(...)})
. Но тогда я потеряю все преимущества интеграции "реакция-аполлон", поэтому на самом деле не вариант.
Вариант 4
Должен ли я вообще не обновлять хранилище Redux после возвращения запроса? Потому что я мог бы просто реализовать селектор, который возвращает selectedItem
, если он установлен, и если он не пытается получить его, просмотрев часть apollo
в хранилище.
Ни один из моих вариантов не удовлетворяет меня. Итак, как бы я сделал это правильно?
Ответы
Ответ 1
Я бы сделал что-то похожее на Вариант 2, но поместил методы жизненного цикла в фактический компонент. Таким образом, бизнес-логика жизненного цикла будет отделена от реквизита, унаследованного от Container.
Так что-то вроде этого:
class yourComponent extends Component{
componentWillReceiveProps(nextProps) {
const {
loading,
items,
selectedItem,
selectItem,
} = nextProps;
if (!selectedItem && !loading && items && items.length) {
selectItem(items[items.length - 1].id);
}
}
render(){...}
}
// Connect redux and graphQL to the Component
const yourComponentWithGraphQL = graphql(...)(yourComponent);
export default connect(mapStateToProps, mapDispatchToProps)(yourComponentWithGraphQL)
Ответ 2
должно быть достаточно использовать "реквизит", например:
const enhance = compose(
connect(
function mapStateToProps(state) {
return {
selectedItem: getSelectedItem(state),
};
}, {
selectItem, // action creator
}
),
graphql(
dataListQuery, {
options: ({ listId }) => ({
variables: {
listId,
},
}),
props: ({ data: { loading, dataList } }) => {
if (!loading && dataList && dataList.length) {
selectItem(dataList[dataList.length - 1].id);
}
return {
loading,
items: dataList,
}
},
}
),
);
Ответ 3
Я бы прослушал изменения в componentDidUpdate и, когда они произошли, отправил действие, которое установит selectedItem в хранилище Redux
componentDidUpdate(prevProps, prevState) {
if (this.props.data !== prevProps.data) {
dispatch some action that set whatever you need to set
}
}
Ответ 4
Я использую hoc, который является немного лучшей версией варианта 2. Я использую withLoader Hoc в конце compose.
const enhance = compose(
connect(),
graphql(dataListQuery, {
options: ({ listId }) => ({
variables: {
listId,
},
}),
props: ({ data: { loading, dataList } }) => ({
isLoading:loading,
isData:!!dataList,
dataList
}),
}
),
withLoader
)(Component)
WithLoader hoc компонент рендеринга, основанный на двух свойствах isData и isLoading. Если isData истина, тогда это рендерит Wrapped Component, иначе рендеринг загрузчик.
function withLoader(WrappedComponent) {
class comp extends React.PureComponent {
render(){
return this.props.isData?<WrappedComponent {...this.props}/>:<Loading/>
}
}
}
Я устанавливаю первый элемент dataList в методе Component componentWillMount. Компонент не монтируется, пока мы не получим dataList, который обеспечивается withLoader hoc.
Ответ 5
На мой взгляд, лучший подход - это создать слегка модифицированную и компонуемую версию hoc варианта 2, которая будет использоваться аналогично graphql
hoc. Вот пример использования, который приходит на ум:
export default compose(
connect(
state => ({ /* ... */ }),
dispatch => ({
someReduxAction: (payload) => dispatch({ /* ... */ }),
anotherReduxAction: (payload) => dispatch({ /* ... */ }),
}),
),
graphqlWithDone(someQuery, {
name: 'someQuery',
options: props => ({ /* ... */ }),
props: props => ({ /* ... */ }),
makeDone: props => dataFromQuery => props.someReduxAction(dataFromQuery)
}),
graphqlWithDone(anotherQuery, {
name: 'anotherQuery',
options: props => ({ /* ... */ }),
props: props => ({ /* ... */ }),
makeDone: props => dataFromQuery => props.anotherReduxAction(dataFromQuery)
})
)(SomeComponent)
И самая простая реализация будет выглядеть примерно так:
const graphqlWithDone = (query, queryConfig) => (Wrapped) => {
const enhance = graphql(query, {
...queryConfig,
props: (props) => ({
queryData: { ...( props[queryConfig.name] || props.data ) },
queryProps: queryConfig.props(props),
})
})
class GraphQLWithDone extends Component {
state = {
isDataHandled: false
}
get wrappedProps () {
const resultProps = { ...this.props };
delete resultProps.queryData;
delete resultProps.queryProps;
return { ...resultProps, ...this.props.queryProps }
}
get shouldHandleLoadedData () {
return (
!this.props.queryData.error &&
!this.props.queryData.loading &&
!this.state.isDataHandled
)
}
componentDidUpdate() {
this.shouldHandleLoadedData &&
this.handleLoadedData(this.props.queryData);
}
handleLoadedData = (data) => {
if (!makeDone || !isFunction(makeDone)) return;
const done = makeDone(this.wrappedProps)
this.setState({ isDataHandled: true }, () => { done(data) })
}
render() {
return <Wrapped {...this.wrappedProps} />
}
}
return enhance(GraphQLWithDone)
}
Даже при том, что я не пробовал этот псевдокод, у него нет тестов и даже не закончен, идея этого довольно проста и легка для понимания. Надеюсь, это кому-нибудь поможет
Ответ 6
В прошлом я сталкивался с подобной проблемой и выбирал что-то похожее на вариант 2. Если у вас есть и собственное хранилище с избыточностью, и собственное внутреннее хранилище apollo, состояние синхронизации между ними становится проблемой.
Я бы посоветовал избавиться от собственного магазина редуксов, если вы используете apollo. Если вы используете сервер gql и несколько остальных серверов одновременно, разделите данные логически и физически.
Как только вы решите использовать apollo в качестве "источника данных", диспетчеризация - это просто мутация, а получение состояния - просто запрос. Вы также можете фильтровать, сортировать и т.д. С запросами