📜 ⬆️ ⬇️

Reduce the use of Redux code with React Apollo

Hello! I want to share my translation of an interesting article Reducing our Redux code with React Apollo by Peggy Rayzis . An article about how the author and her team implemented GraphQL technology in their project. The translation is published with the permission of the author.


Reduce the use of Redux code with React Apollo
Switch to declarative approach in the Higher Football League


I firmly believe that the best code is the lack of code. The greater the code, the greater the likelihood for the appearance of bugs and the more time is spent on supporting such a code. In the Higher Football League we have a very small team, so we take this principle to heart. We try to optimize everything that we can do, either by increasing the reusability of the code, or simply ceasing to serve a certain part of the code.


In this article, you will learn how we transferred control of data acquisition to Apollo, which allowed us to get rid of almost 5,000 lines of code . In addition, after switching to Apollo, our application became not only much smaller in volume, it also became more declarative , since now our components request only the data they need.


What do I mean by declarative and why is it so great? Declarative programming focuses on the final goal, while imperative programming focuses on the steps that are required to achieve it. React itself is declarative.


Retrieving Data with Redux


Let's take a look at the simple component of Article:


import React from 'react';
import { View, Text } from 'react-native';
export default ({ title, body }) => (
<View>
<Text>{title}</Text>
<Text>{body}</Text>
</View>
);
view raw article.js hosted with ❤ by GitHub

Suppose we want to render <Article/> in a connected <MatchDetail/> view that receives the match ID as props. If we did this without a GraphQL client, our process for obtaining the data needed to render <Article/> could be:


  1. When <MatchDetail/> is mounted, call the action creator to get the match data by ID. The Action creator action dispatch to tell Redux about the start of the data acquisition process.
  2. We reached the destination and returned with the data. We normalize the data in a convenient structure.
  3. After the data is normalized, we dispatch another action to tell Redux that the data acquisition process is complete.
  4. Redux handles the action in our reducer and updates the state of the application.
  5. <MatchDetail/> gets all the necessary match data via props and filters it to render the article.

So many steps to just get the data for <Article/> ! Without a GraphQL client, our code becomes much more imperative because we have to concentrate on how we get the data. But what if we don’t want to transfer all the match data for a simple rendering of <Article/> ? You could build another endpoint and create a separate set of action creators to get data from it, but this option can very easily become unsupported.


Let's compare how we could do the same with GraphQL:


  1. <MatchDetail/> connected to a higher-order component that performs the following query:

 query Article($id: Float!) { match(id: $id) { article { title body } } } 

… and it's all! As soon as the client receives the data, he sends them to the props, which can be further transferred to <Article/> . This is much more declarative , since we focus only on what data we need to render the component.


That's the beauty of delegating the receipt of GraphQL data to a client, be it Relay or Apollo. When you start thinking in GraphQL concepts , you only care about the props the component needs to render, instead of worrying about how to get this data.


At some point, you will have to think about this " how ", but this is already a concern of the server part, and, accordingly, the complexity for the front-end is sharply reduced. If you are new to GraphQL server architecture, then try graphql-tools , the Apollo library, which will help you structure your circuitry more graphql-tools . For brevity, we will focus today only on the front-end part.


And although this post is about how to reduce the use of your Redux code, you will not get rid of it completely! Apollo uses Redux under the hood, so you can still benefit from immunity and all the cool features of Redux Dev Tools, such as time-travel debugging, will also work. During configuration, you can connect Apollo to an existing store in Redux to maintain a single "source of truth". Once your store is configured, you pass it to the <ApolloProvider/> component that wraps your entire application. Sounds familiar? This component is a complete replacement for your existing <Provider/> from Redux, except that you need to pass an ApolloClient instance to ApolloClient via the client property.


Before we start cutting our Redux code, I would like to name one of the best features of GraphQL: incremental tuning . You do not need to refactor the entire application at once. After integrating Apollo with your existing Redux store, you can switch from your reducers gradually . The same applies to the server side - if you are working on a large scale application, you can use GraphQL side by side with your current REST API, until you are ready for the full transition. A fair warning: as soon as you try GraphQL, you may fall in love with this technology and want to redo your entire application.


Our requirements


Before moving from Redux to Apollo, we carefully thought about whether Apollo meets our needs. Here is what we noticed before making a decision:



I must say that Apollo not only met all of our current requirements, it also covered some of our future needs, especially given the fact that our roadmap includes advanced personalization. And although our server is currently read-only, we may need to introduce mutations in the future in order for users to save their favorite commands. In case we decide to add comments in real time or interact with fans that cannot be resolved using polling, Apollo has support for subscriptions .


From Redux to Apollo


The moment that everyone was waiting for! Initially, when I only thought about writing this article, I was only going to give examples of code before and after, but I think it would be difficult to compare these two approaches directly, especially for newcomers to Apollo. Instead, I’m going to count the amount of remote code as a whole and walk you through the familiar Redux concepts that you can use to create container components with Apollo.


What we removed



connect () → graphql ()


If you know how to use connect , then Apollo’s higher-order graphql component will seem very familiar to you! Just as connect returns a function that accepts a component and connects it to your Redux store, also graphql returns a function that accepts a component and "connects" it to the Apollo client. Let's look at it in action!


import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import { MatchSummary, NoDataSummary } from '@mls-digital/react-components';
import MatchSummaryQuery from './match-summary.graphql';
// here we're using the graphql HOC as a decorator, but you can use it as a function too!
@graphql(MatchSummaryQuery, {
options: ({ id, season, shouldPoll }) => {
return {
variables: {
id,
season,
},
pollInterval: shouldPoll ? 1000 * 60 : undefined,
};
};
})
class MatchSummaryContainer extends Component {
render() {
const { data: { loading, match } } = this.props;
if (loading && !match) {
return <NoDataSummary />;
}
return <MatchSummary {...match} />;
}
}
export default MatchSummaryContainer;
view raw match-summary.js hosted with ❤ by GitHub

The first argument passed to graphql is MatchSummaryQuery . This is the data we want to get from the server. We use the Webpack loader to parse our query into GraphQL AST, but if you don’t use Webpack, you need to wrap your query in template string and pass it to the gql function exported from Apollo. Here is an example of a request to get the data necessary for our component:


query MatchSummary($id: String!, $season: String) {
match(id: $id) {
stats {
scores {
home {
score
isWinner: is_winner
}
away {
score
isWinner: is_winner
}
}
}
home {
id: opta_id
record(season: $season)
}
away {
id: opta_id
record(season: $season)
}
}
}

Great, we have a request! To execute it correctly, we need to pass into it two variables $id and $season . But where do we get these variables from? This is where the second argument of the graphql function comes into play, represented as a configuration object.


This object has several properties that you can specify to customize the behavior of a higher order component (HOC). One of the most important properties is options , which accepts a function that gets the props of your container. This function returns an object with properties of type variables , which allows you to pass your variables to the request, and pollInterval , which allows you to customize the polling behavior of the component. Notice how we use our container's props to transfer id and season to our MatchSummaryQuery . If this function becomes too long to write it directly in the decorator, then we divide it into a separate function called mapPropsToOptions .


mapStateToProps () → mapResultsToProps ()


Surely you used the mapStateToProps function in your Redux containers, passing this function to connect to transfer data from the state application to the props of this container. Apollo allows you to define a similar function. Remember the configuration object that we previously passed to the graphql function? This object has another property - props , which accepts a function that receives props as input and processes them before being transferred to the container. Of course, you can define it directly in graphql , but we like to define it as a separate function mapResultsToProps .


Why do you need to override your props? The result of a GraphQL query is always assigned to the data property. Sometimes you may need to correct this data before sending it to the component. Here is one example:


import React, { Component } from 'react';
import { graphql } from 'react-apollo';
import { MatchSummary, NoDataSummary } from '@mls-digital/react-components';
import MatchSummaryQuery from './match-summary.graphql';
const mapResultsToProps = ({ data }) => {
if (!data.match)
return {
loading: data.loading,
};
const { stats, home, away } = data.match;
return {
loading: data.loading,
home: {
...home,
results: stats.scores.home,
},
away: {
...away,
results: stats.scores.away,
},
};
};
const mapPropsToOptions = ({ id, season, shouldPoll }) => {
return {
variables: {
id,
season,
},
pollInterval: shouldPoll ? 1000 * 60 : undefined,
};
};
@graphql(MatchSummaryQuery, {
props: mapResultsToProps,
options: mapPropsToOptions,
})
class MatchSummaryContainer extends Component {
render() {
const { loading, ...matchSummaryProps } = this.props;
if (loading && !matchSummaryProps.home) {
return <NoDataSummary />;
}
return <MatchSummary {...matchSummaryProps} />;
}
}
export default MatchSummaryContainer;
view raw match-summary.js hosted with ❤ by GitHub

Now the data object contains not only the result of the query, but also properties of the data.loading type to let you know that the query has not yet returned an answer. This can be useful if in a similar situation you want to display a different component to your users, as we did with <NoDataSummary/> .


compose ()


Compose is a function that is used not only in Redux, but I still want to draw your attention to the fact that Apollo contains it. It is very convenient if you want to build several graphql functions for use in one container. In it, you can even use the connect function from Redux along with graphql ! Here is how we use compose to display various states of a match:


import React, { Component } from 'react';
import { compose, graphql } from 'react-apollo';
import { NoDataExtension } from '@mls-digital/react-components';
import PostGameExtension from './post-game';
import PreGameExtension from './pre-game';
import PostGameQuery from './post-game.graphql';
import PreGameQuery from './pre-game.graphql';
@compose(
graphql(PreGameQuery, {
skip: ({ gameStatus }) => gameStatus !== 'pre',
props: ({ data }) => ({
preGameLoading: data.loading,
preGameProps: data.match,
}),
}),
graphql(PostGameQuery, {
skip: ({ gameStatus }) => gameStatus !== 'post',
props: ({ data }) => ({
postGameLoading: data.loading,
postGameProps: data.match,
}),
}),
)
export default class MatchExtensionContainer extends Component {
render() {
const {
preGameLoading,
postGameLoading,
gameStatus,
preGameProps,
postGameProps,
...rest
} = this.props;
if (preGameLoading || postGameLoading)
return <NoDataExtension gameStatus={gameStatus} />;
return gameStatus === 'post'
? <PostGameExtension {...postGameProps} {...rest} />;
: <PreGameExtension {...preGameProps} {...rest} />;
}
}

compose great when your container contains multiple states. But what if you only need to perform a separate query depending on its state? Here skip will help us, which you can see in the configuration object above. The skip property accepts a function that gets props and allows you to skip query execution if it does not meet the required criteria.


All these examples demonstrate that if you know Redux, then you will quickly be pushed into the development of Apollo! Its API has absorbed many of the concepts of Redux, while reducing the amount of code you need to write to achieve the same result.




I hope that the experience of the transition of the Higher Football League to Apollo will help you! As is the case with any decisions regarding different libraries, the best solution for controlling the receipt of data in your application will depend on the specific requirements of your project. If you have any questions about our experience, please leave comments here or knock me on Twitter !


Thanks for reading!


')

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


All Articles