Migrating from Redux to GraphQL: A NerdWallet Internship Experience
The following article is part of a series of articles about our NerdWallet Summer Internship program. Ming Horn is a student at Mills College and shared their experience as an software engineer intern. If you are curious about joining NerdWallet as an intern or full-time employee, please apply for one of our open positions!
This summer, I’ve worked as a software engineering intern at NerdWallet. I was part of the Mobile Core team, which helps build out the ‘core’ of the NerdWallet mobile app and support mobile feature engineers. My main project was working on writing Apollo GraphQL schema and migrating several screens from Redux to Apollo GraphQL.
Here are some of my tips and learnings about migrating to Apollo GraphQL.
Basic context:
In Redux, events in the view dispatch actions. The store dispatcher handles the actions and combines data from the API and current stored state through reducers to calculate the updated state that the view needs.
In GraphQL the view makes queries or mutations which the Apollo Client (or any other GraphQL client) then parses and checks if the data already exists in the cache. If not, it makes a request to the Apollo Server, the schema maps the query to its resolvers, which request data from the APIs and returns it in the shape dictated by the schema. The request then resolves and the returned data is stored in the cache for future use and the data is returned to the view.
Very roughly:
Actions = queries, mutations, or subscriptions from client
Selectors = query resolvers
Reducers = mutation resolvers
Store State = Apollo cache
Disclaimer: I am assuming you know some basics about Apollo GraphQL and Redux, and have set up your Apollo Server and Apollo Client provider in your app. There are a myriad of other articles about how to do that.
This is what you want to migrate first: for example, I migrated the globalProfile which encompasses data like your firstName and lastName.
I would recommend choosing some basic foundational types before higher-order types. Types that have one or two levels of nesting typically are the right ones to choose.
Look at a screen that uses the data that you identified in the first step. Start by inspecting the selectors it uses. What other data does it need? Can most of it come from one data source?
Let’s say you have a component connected to state below:
const mapStateToProps = state => ({
feed: getDisplayableFeed(state),
firstName: getUserFirstName(state),
lastName: getUserLastName(state),
});
const mapDispatchToProps = dispatch => ({
fetchFeed: () => dispatch(getFeed()),
});
export default connect(
mapStateToProps,
mapDispatchToProps,
)(SomeScreen);
So you would want to write a fetchFeed or getDisplayableFeed query, and if getUserFirstName and getUserLastName come from the globalProfile, you would want to write a getGlobalProfile query.
Now that you know what data you need to fetch and how it should render you can write the schema. Here you’ll define the different data types — these are basically models; typically they’re organized based on how you will make a request. Then you define queries and mutations based on what you learned in Step 2. For example:
export default {
typedefs gql`
type GlobalProfile {
userId: ID!
firstName: String
lastName: String
}
type FeedCard {
id: ID!
title: String!
}
type Feed {
id: ID!
feedCards: [FeedCard!]!
}
extend type Query {
getGlobalProfile(userId: ID!): GlobalProfile
getDisplayableFeed(filter: String): Feed
}
`,
resolvers: {
Query: {
getGlobalProfile: async (_, { id }, { dataSources }) => {
return await fetchProfile(dataSources, id);
},
getDisplayableFeed: async (_, { filter }, _) => {
const feed = await this.post('feed_endpoint');
// whatever filtering logic here
return _.filter(feed, item => return item.title.equals(filter));
},
},
GlobalProfile: {
firstName: data => _.get('first_name'),
lastName: data => _.get('last_name'),
},
},
};
Resolvers are what actually fetch data for the queries, mutations, and types. There are lots of great articles about how to write them efficiently so that the number of server requests to APIs are minimized.
Further reading:
Now you’re ready to write your first query! Test it out using Prisma Playground or GraphiQL. The power of it is that you don’t need to select every field from a query, only what your client needs.
query GetScreenInfo {
getGlobalProfile(id: 123) {
firstName
lastName
}
getDisplayableFeed(filter: 'Migrate Redux to GraphQL') {
feedCards {
title
}
}
}
Sometimes there’s more complex situations when migrating…
There were two main issues I ran into migrating an app implemented with Redux to GraphQL. One, I didn’t write schema for every piece of data loaded by a screen so some still needed to come from the Redux store. Two, components rendering data loaded from the Redux store expect to have it all immediately without a loading state.
Additionally simply wrapping the component in a Query component seemed a little awkward or impossible if there were more HOCs that did anything remotely complex like dynamically render form fields based on the Redux state.
The solution was a HOC! I created a withApolloQuery HOC that wrapped the container in a Query component. I could then just write:
export default withApolloQuery(
connectForm(mapStateToProps)(SomeComponent),
query,
variables,
options
);
Further Reading:
Thank yous:
Thanks to the Mobile Core team: Casey, Steve, Martin, Rahat, Eric, and Sam, for an amazing summer, I had a great time being your Nerdling!