Πώς να χρησιμοποιήσετε το GraphQL στην εφαρμογή Redux

Η ανάκτηση και διαχείριση δεδομένων στο Redux απαιτεί πάρα πολλή δουλειά. Όπως επισημαίνει ο Sashko Stubailo:

Δυστυχώς, τα μοτίβα για ασύγχρονη φόρτωση δεδομένων διακομιστή σε μια εφαρμογή Redux δεν είναι τόσο καλά καθιερωμένα, και συχνά περιλαμβάνουν τη χρήση εξωτερικών βοηθητικών βιβλιοθηκών, όπως το redux-saga. Πρέπει να γράψετε προσαρμοσμένο κώδικα για να καλέσετε τα τελικά σημεία του διακομιστή σας, να ερμηνεύσετε τα δεδομένα, να τα ομαλοποιήσετε και να τα εισαγάγετε στο κατάστημα - όλα διατηρώντας παράλληλα διάφορες καταστάσεις σφαλμάτων και φόρτωσης.

Μέχρι το τέλος αυτού του σεμιναρίου, θα έχετε μάθει πώς να επιλύσετε αυτό το πρόβλημα αφήνοντας τον Πελάτη Apollo να πάρει και να διαχειριστεί δεδομένα για εσάς. Δεν θα χρειάζεται πλέον να γράφετε πολλαπλούς διεκπεραιωτές δράσης, μειωτές και κανονικοποιητές για τη λήψη και τον συγχρονισμό δεδομένων μεταξύ της διεπαφής σας και της πίσω πλευράς σας.

Αλλά πριν ξεκινήσετε το σεμινάριο, βεβαιωθείτε ότι:

  • Γνωρίζετε τα βασικά των ερωτημάτων GraphQL - εάν είστε εντελώς νέοι στη GraphQL, θα πρέπει να επιστρέψετε αφού κάνετε αυτό το σεμινάριο.
  • Έχετε κάποια εμπειρία εργασίας με το React / Redux - εάν όχι, θα πρέπει να επιστρέψετε μετά την εκμάθηση του react tutorial και του redux.

Σε αυτό το σεμινάριο, θα εξετάσουμε 6 ενότητες μαζί.

  1. Ρύθμιση περιβάλλοντος διακομιστή (γρήγορο)
  2. Ρύθμιση εφαρμογής redux boilerplate
  3. Προσθήκη προγράμματος-πελάτη GraphQL (Apollo Client)
  4. Ανάκτηση δεδομένων με ερώτημα GraphQL
  5. Λήψη ακόμη περισσότερων δεδομένων
  6. Επόμενα βήματα

1. Ρύθμιση περιβάλλοντος διακομιστή

Πρώτον, χρειαζόμαστε έναν διακομιστή GraphQL. Ο ευκολότερος τρόπος για να διαθέτετε διακομιστή είναι να ολοκληρώσετε αυτό το φοβερό σεμινάριο.

Εάν αισθάνεστε τεμπέλης, μπορείτε απλώς να κλωνοποιήσετε το repo μου, ο οποίος είναι σχεδόν ο ίδιος διακομιστής που θα λάβατε εάν κάνατε μόνοι σας το σεμινάριο Ο διακομιστής υποστηρίζει ερωτήματα GraphQL για τη λήψη δεδομένων από ένα SQLite DB.

Ας το τρέξουμε και να δούμε αν λειτουργεί σωστά:

$ git clone //github.com/woniesong92/apollo-starter-kit$ cd apollo-starter-kit$ npm install$ npm start

Ο διακομιστής θα πρέπει να εκτελείται στη διεύθυνση // localhost: 8080 / graphql. Πλοηγηθείτε σε αυτήν τη σελίδα και δείτε εάν έχετε μια λειτουργική διεπαφή GraphiQL με αποτελέσματα όπως αυτό:

Το GraphiQL σάς επιτρέπει να δοκιμάζετε διαφορετικά ερωτήματα και να βλέπετε αμέσως ποια απάντηση λαμβάνετε από τον διακομιστή. Εάν δεν θέλουμε το επώνυμο ενός συντάκτη και ένα μήνυμα cookie τύχης σε απάντηση, μπορούμε να ενημερώσουμε το ερώτημα όπως παρακάτω:

Και έτσι μας αρέσει. Επιβεβαιώσαμε ότι ο διακομιστής μας λειτουργεί καλά και επιστρέφει καλές απαντήσεις, οπότε ας αρχίσουμε να χτίζουμε τον πελάτη.

2. Εγκατάσταση εφαρμογής redux boilerplate

Για απλότητα, θα χρησιμοποιήσουμε ένα redux boilerplate ώστε να μπορούμε να λάβουμε όλη τη ρύθμιση (π.χ. Babel, webpack, CSS κ.λπ.) δωρεάν. Μου αρέσει αυτό το boilerplate επειδή η ρύθμιση είναι εύκολο να ακολουθηθεί και είναι μόνο από την πλευρά του πελάτη - κάτι που το καθιστά ιδανικό για αυτό το σεμινάριο.

$ git clone //github.com/woniesong92/react-redux-starter-kit.git$ cd react-redux-starter-kit$ npm install$ npm start

Ας μεταβούμε στο // localhost: 3000 / για να δούμε αν εκτελείται ο διακομιστής-πελάτης.

Ναι! Ο πελάτης εκτελείται. Ήρθε η ώρα να αρχίσουμε να προσθέτουμε έναν πελάτη GraphQL. Και πάλι, ο στόχος μας είναι η εύκολη ανάκτηση δεδομένων από το διακομιστή και η απόδοσή τους στη σελίδα προορισμού (HomeView) χωρίς πολλή προσπάθεια χρησιμοποιώντας ερωτήματα GraphQL.

3. Προσθήκη προγράμματος-πελάτη GraphQL (Apollo Client)

Εγκαταστήστε τα πακέτα apollo-client, react-apollo και graphql-tag.

$ npm install apollo-client react-apollo graphql-tag --save

Στη συνέχεια, ανοίξτε το αρχείο src / container / AppContainer.js, τη ρίζα της εφαρμογής Redux. Εδώ περνάμε το κατάστημα redux σε εξαρτήματα για παιδιά, χρησιμοποιώντας τον Πάροχο από το react-redux.

import React, { PropTypes } from 'react'import { Router } from 'react-router'import { Provider } from 'react-redux'
class AppContainer extends React.Component { static propTypes = { history: PropTypes.object.isRequired, routes: PropTypes.object.isRequired, routerKey: PropTypes.number, store: PropTypes.object.isRequired }
render () { const { history, routes, routerKey, store } = this.props
return ( ) }}
export default AppContainer

Πρέπει να προετοιμάσουμε ένα ApolloClient και να αντικαταστήσουμε τον Πάροχο από το react-redux με το ApolloProvider από το react-apollo.

import React, { Component, PropTypes } from 'react'import { Router } from 'react-router'import ApolloClient, { createNetworkInterface, addTypename } from 'apollo-client'import { ApolloProvider } from 'react-apollo'
const client = new ApolloClient({ networkInterface: createNetworkInterface('//localhost:8080/graphql'), queryTransformer: addTypename,})
class AppContainer extends Component { static propTypes = { history: PropTypes.object.isRequired, routes: PropTypes.object.isRequired, store: PropTypes.object.isRequired }
render () { const { history, routes } = this.props
return ( ) }}
export default AppContainer

Αυτό είναι! Μόλις προσθέσαμε έναν πελάτη GraphQL σε μια απλή εφαρμογή Redux που εύκολα.

Ας προχωρήσουμε και δοκιμάστε το πρώτο μας ερώτημα GraphQL.

4. Fetching data with GraphQL queries

Open src/views/HomeView.js

import React from 'react'import { connect } from 'react-redux'import { bindActionCreators } from 'redux'
export class HomeView extends React.Component { constructor(props) { super(props) }
render () { return ( 

Hello World

) }}
// This is where you usually retrieve the data stored in the redux store (e.g posts: state.posts.data)const mapStateToProps = (state, { params }) => ({
})
// This is where you usually bind dispatch to actions that are used to request data from the backend. You will call the dispatcher in componentDidMount.const mapDispatchToProps = (dispatch) => { const actions = {}
 return { actions: bindActionCreators(actions, dispatch) }}
export default connect( mapStateToProps, mapDispatchToProps)(HomeView)

HomeView is a conventional Redux container (smart component). To use GraphQL queries instead of action dispatchers to fetch data, we will make some changes together.

  1. Remove mapDispatchToProps() and mapStateToProps() completely.
import React from 'react'import { connect } from 'react-redux'import { bindActionCreators } from 'redux'
export class HomeView extends React.Component { constructor(props) { super(props) }
 render () { return ( 

Hello World

) }}
export default connect({
})(HomeView)

2. Add mapQueriesToProps() and define a GraphQL query that will fetch the author information. Notice how this is the exactly the same query that we tested in the beginning using the GraphIQL interface on the server.

import React from 'react'import { connect } from 'react-redux'import { bindActionCreators } from 'redux'
export class HomeView extends React.Component { constructor(props) { super(props) }
 render () { return ( 

Hello World

) }}
// NOTE: This will be automatically fired when the component is rendered, sending this exact GraphQL query to the backend.const mapQueriesToProps = ({ ownProps, state }) => { return { data: { query: gql` query { author(firstName:"Edmond", lastName: "Jones"){ firstName posts { title } } } ` } }}
export default connect({
})(HomeView)

3. Replace connect from react-redux with connect from react-apollo and pass mapQueriesToProps as argument. Once mapQueriesToProps is connected to ApolloClient, the query will automatically fetch the data from the backend when HomeView is rendered, and pass the data down through props.

import React from 'react'import { connect } from 'react-apollo' // NOTE: different connect!import gql from 'graphql-tag' // NOTE: lets us define GraphQL queries in a template language
export class HomeView extends React.Component { constructor(props) { super(props) }
render () { return ( 

Hello World

) }}
const mapQueriesToProps = ({ ownProps, state }) => { return { data: { query: gql` query { author(firstName:"Edmond", lastName: "Jones"){ firstName posts { title } } } ` } }}
export default connect({ mapQueriesToProps})(HomeView)

4. Render the data that’s passed down from the props:

import React from 'react'import { connect } from 'react-apollo' // NOTE: different connect!import gql from 'graphql-tag' // NOTE: lets us define GraphQL queries in a template language
export class HomeView extends React.Component { constructor(props) { super(props) }
 render () { const author = this.props.data.author if (!author) { return 

Loading

}
 return ( 

{author.firstName}'s posts

{author.posts && author.posts.map((post, idx) => (
  • {post.title}
  • ))} ) }}
    const mapQueriesToProps = ({ ownProps, state }) => { return { data: { query: gql` query { author(firstName:"Edmond", lastName: "Jones"){ firstName posts { title } } } ` } }}
    export default connect({ mapQueriesToProps})(HomeView)

    If all went well, your rendered HomeView should look like below:

    To fetch and render the data we wanted, we didn’t have to write any action dispatcher, reducer, or normalizer. All we had to do on the client was to write a single GraphQL query!

    We successfully achieved our initial goal. But that query was quite simple. What if we wanted to display all authors instead of just one author?

    5. Fetching even more data

    In order to fetch and display all authors, we have to update our GraphQL query and render method:

    import React from 'react'import { connect } from 'react-apollo' // NOTE: different connect!import gql from 'graphql-tag' // NOTE: lets us define GraphQL queries in a template language
    export class HomeView extends React.Component { constructor(props) { super(props) }
    render () { const authors = this.props.data.authors if (!authors) { return 

    Loading

    }
     return ( {authors.map((author, idx) => ( 

    {author.firstName}'s posts

    {author.posts && author.posts.map((post, idx) => (
  • {post.title}
  • ))} ))} ) }}
    const mapQueriesToProps = ({ ownProps, state }) => { return { data: { query: gql` query { authors { firstName posts { title } } } ` } }}
    export default connect({ mapQueriesToProps})(HomeView)

    However, once you refresh your browser HomeView page, you will notice that you have an error in your console:

    ApolloError {graphQLErrors: Array[1], networkError: undefined, message: “GraphQL error: Cannot query field “authors” on type “Query”. Did you mean “author”?”}

    Ah, right! In our GraphQL server, we didn’t really define how to fetch authors.

    Let’s go back to our server and see what we have. Open the file apollo-starter-kit/data/resolvers.js

    import { Author, FortuneCookie } from './connectors';
    const resolvers = { Query: { author(_, args) { return Author.find({ where: args }); }, getFortuneCookie() { return FortuneCookie.getOne() } }, Author: { posts(author) { return author.getPosts(); }, }, Post: { author(post) { return post.getAuthor(); }, },};
    export default resolvers;

    Looking at Query resolver, we notice that our GraphQL server only understands author and getFortuneCookie queries now. We should teach it how to “resolve” the query authors.

    import { Author, FortuneCookie } from './connectors';
    const resolvers = { Query: { author(_, args) { return Author.find({ where: args }); }, getFortuneCookie() { return FortuneCookie.getOne() }, authors() { // the query "authors" means returning all authors! return Author.findAll({}) } }, ...};
    export default resolvers;

    We are not done yet. Open the file apollo-starter-kit/data/schema.js

    const typeDefinitions = `...
    type Query { author(firstName: String, lastName: String): Author getFortuneCookie: String}schema { query: Query}`;
    export default [typeDefinitions];

    This Schema makes it clear what kind of queries the server should expect. It doesn’t expect authors query yet so let’s update it.

    const typeDefinitions = `...
    type Query { author(firstName: String, lastName: String): Author getFortuneCookie: String, authors: [Author] // 'authors' query should return an array of // Author}schema { query: Query}`;
    export default [typeDefinitions];

    Now that our GraphQL server knows what the “authors” query means, let’s go back to our client. We already updated our query so we don’t have to touch anything.

    export class HomeView extends React.Component {
    ...
    const mapQueriesToProps = ({ ownProps, state }) => { return { data: { query: gql` query { authors { firstName posts { title } } } ` } }}
    export default connect({ mapQueriesToProps})(HomeView)

    Με αυτό το ερώτημα αναμένουμε να πάρουμε σε όλους τους συγγραφείς τα ονόματα και τις αναρτήσεις τους. Προχωρήστε και ανανεώστε το πρόγραμμα περιήγησης για να δείτε αν λαμβάνουμε τα σωστά δεδομένα.

    Αν όλα πάνε καλά, η αρχική σελίδα σας θα μοιάζει παραπάνω.

    6. Επόμενα βήματα

    Αυτό το σεμινάριο εξερευνά μόνο ένα μικρό μέρος της GraphQL και αφήνει πολλές έννοιες όπως η ενημέρωση δεδομένων στο διακομιστή ή η χρήση διαφορετικού διακομιστή backend (π.χ. Rails).

    Ενώ εργάζομαι για να τα παρουσιάσω σε επόμενα σεμινάρια, μπορείτε να διαβάσετε την ανάρτηση του Sashko ή το Apollo Client Doc για να κατανοήσετε καλύτερα τι συμβαίνει κάτω από την κουκούλα (για παράδειγμα, τι συνέβη όταν αντικαταστήσαμε το Provider με το ApolloProvider;)

    Η ανίχνευση στον πηγαίο κώδικα του GitHunt, ενός παραδείγματος εφαρμογής Apollo Client και Server πλήρους στοίβας, φαίνεται επίσης ένας πολύ καλός τρόπος για να μάθετε.

    Εάν έχετε σχόλια, αφήστε το στο σχόλιο. Θα προσπαθήσω το καλύτερό μου για να βοηθήσω :)