How to write a React component that fetches data from back-end?
In this guide, you will learn how to write a component that fetches data from the backend using a GraphQL API.
This part of the documentation assumes that you have already created the basic component structure. If you don't have any React component created, please follow the Example React component guide and create it.
Execute query and display data
Before you can fetch data for the component, you need to write a GraphQL query. For the purpose of this guide, we will
assume that there is a todo
object defined in the GraphQL schema and that you want to query for its id
and name
.
Create the following file with the query:
import { gql } from '@shipfast/webapp-api-client/graphql';
export const todoQuery = gql(/* GraphQL */ `
query todoQuery {
todo {
id
name
}
}
`);
Notice that it uses gql
from the @shipfast/webapp-api-client/graphql
package, which is a generated object based on the
available schema.
Now you can modify the component file to use the above query and fetch the data:
import { useQuery } from '@apollo/client';
import { FormattedMessage } from 'react-intl';
import { todoQuery } from './example.graphql';
import { Container } from './example.styles';
export const ExampleComponent = () => {
const { loading, data } = useQuery(todoQuery);
return (
<Container>
{!loading && data ? (
`Fetched todo: ${data.todo?.id} ${data.todo?.name}`
) : (
<FormattedMessage defaultMessage="Loading..." id="ExampleComponent / loading" />
)}
</Container>
);
};
The code above will use the useQuery
hook from
the Apollo library and display the data inside the component.
Note that because of how Apollo works, an XHR request with a query might not be fired in favor of cached data in its internal store.
Modify storybook
Changes made above will break storybook for ExampleComponent
. You can address them by following these changes:
import { composeMockedQueryResult } from '@shipfast/webapp-api-client/tests/utils';
import { Story } from '@storybook/react';
import { append } from 'ramda';
import { withProviders } from '../../utils/storybook';
import { ExampleComponent } from './example.component';
import { todoQuery } from './example.graphql';
const Template: Story = () => {
return <ExampleComponent />;
};
const todo = { id: 1, name: 'Todo' };
export default {
title: 'Shared/ExampleComponent',
component: ExampleComponent,
decorators: [
withProviders({
apolloMocks: append(
composeMockedQueryResult(todoQuery, {
data: {
todo
}
})
),
}),
],
};
export const Default = Template.bind({});
The code above uses some internal ShipFast helpers that aid in writing Storybooks and tests efficiently:
composeMockedQueryResult
: This is a helper that prepares a mocked query result for the given query object for the ApolloMockedProvider
. You can read more about mocking queries here.withProviders
: This is a helper that wraps a Storybook story with the necessary providers (includingMockedProvider
).
The modified Storybook code will wrap every story in this file with the necessary providers that inject a mock query result, allowing the component to load data defined in the story.
Testing changes
Last thing to modify are the tests. Current test doesn't cover loading data and render on loading state. We would like to test if it displays correct data after query fetch with success. At the end of the test you can see assertions for loading text and first name that checks if they are rendered correctly.
For more information about testing loading state using Apollo please check the docs.
import { composeMockedQueryResult } from '@shipfast/webapp-api-client/tests/utils';
import { screen } from '@testing-library/react';
import { append } from 'ramda';
import { render } from '../../../../tests/utils/rendering';
import { ExampleComponent } from '../example.component';
describe('ExampleComponent: Component', () => {
const Component = () => <ExampleComponent />;
it('should render user name', async () => {
const todo = { id: 1, name: 'Todo' };
const loadingText = 'Loading...';
render(<Component />, { apolloMocks: append(composeMockedQueryResult({
data: {
todo
}
}))});
expect(await screen.findByText(loadingText)).toBeInTheDocument();
expect(await screen.findByText(firstName)).toBeInTheDocument();
});
});
Note that this test example uses custom render
method.
For more information about testing web application components, please visit the Writing tests for webapp section.
Working with collections
In the second part of this guide, you will learn how to fetch collections from a GraphQL API.
As in the previous part of this guide, we will assume that there is a collection of todos
in the GraphQL schema and
that the component will fetch a list of items from this collection.
Modify query
import { gql } from '@shipfast/webapp-api-client/graphql';
export const todosQuery = gql(/* GraphQL */ `
query todosQuery {
todos {
edges {
node {
id
name
}
}
}
}
`);
Modify the component
To display a collection of items, you can use the mapConnection
helper. This helper is used to transform a GraphQL
connection object into an array of objects.
import { useQuery } from '@apollo/client';
import { mapConnection } from '@shipfast/webapp-core/utils/graphql';
import { FormattedMessage } from 'react-intl';
import { todosQuery } from './example.graphql';
import { Container } from './example.styles';
export const ExampleComponent = () => {
const { loading, data } = useQuery(todosQuery);
return (
<Container>
{!loading && data ? (
mapConnection((node) => <div key={node?.id}>{node?.name}</div>, data.todos)
) : (
<FormattedMessage defaultMessage="Loading..." id="ExampleComponent / loading" />
)}
</Container>
);
};
Modify storybook and tests
The last thing to do is to modify storybook and tests to handle collection of items:
import { composeMockedListQueryResult } from '@shipfast/webapp-api-client/tests/utils';
import { Story } from '@storybook/react';
import { append } from 'ramda';
import { withProviders } from '../../utils/storybook';
import { ExampleComponent } from './example.component';
import { todosQuery } from './example.graphql';
const Template: Story = () => {
return <ExampleComponent />;
};
const todos = [{ id: 1, name: 'Todo 1' }, { id: 2, name: 'Todo 2' }];
export default {
title: 'Shared/ExampleComponent',
component: ExampleComponent,
decorators: [
withProviders({
apolloMocks: append(
composeMockedListQueryResult(todosQuery,
'todos',
'todosQuery',
{
data: todos
})
),
}),
],
};
export const Default = Template.bind({});
import { composeMockedListQueryResult } from '@shipfast/webapp-api-client/tests/utils';
import { screen } from '@testing-library/react';
import { append } from 'ramda';
import { render } from '../../../../tests/utils/rendering';
import { ExampleComponent } from '../example.component';
describe('ExampleComponent: Component', () => {
const Component = () => <ExampleComponent />;
it('should render list', async () => {
const todos = [{ id: 1, name: 'Todo 1' }, { id: 2, name: 'Todo 2' }];
const loadingText = 'Loading...';
render(<Component />, {
apolloMocks: append(
composeMockedListQueryResult(todosQuery, 'todos', 'todosQuery', { data: todos })
),
});
// check if the loaded is rendered
expect(await screen.findByText(loadingText)).toBeInTheDocument();
// next, check if the first name is rendered correctly
expect(await screen.findByText(todos[0].name)).toBeInTheDocument();
});
});