We are building an offline first React Native Application with Apollo Client. Currently I am trying to update the Apollo Cache directly when offline to update the UI optimistically. Since we offline we do not attempt to fire the mutation until connect is "Online" but would like the UI to reflect these changes prior to the mutation being fired while still offline. We are using the readQuery / writeQuery API functions from http://dev.apollodata.com/core/read-and-write.html#writequery-and-writefragment. and are able to view the cache being updated via Reacotron, however, the UI does not update with the result of this cache update.
const newItemQuantity = existingItemQty + 1;
const data = this.props.client.readQuery({ query: getCart, variables: { referenceNumber: this.props.activeCartId } });
data.cart.items[itemIndex].quantity = newItemQuantity;
this.props.client.writeQuery({ query: getCart, data });
If you look at the documentation examples, you will see that they use the data in an immutable way. The data attribute passed to the write query is not the same object as the one that is read. Mutating this object is unlikely to be supported by Apollo because it would not be very efficient for it to detect which attributes you modified, without doing deep copies and comparisons of data before/after.
const query = gql`
query MyTodoAppQuery {
todos {
id
text
completed
}
}
`;
const data = client.readQuery({ query });
const myNewTodo = {
id: '6',
text: 'Start using Apollo Client.',
completed: false,
};
client.writeQuery({
query,
data: {
todos: [...data.todos, myNewTodo],
},
});
So you should try the same code without mutating the data. You can use for example set of lodash/fp to help you
const data = client.readQuery({...});
const newData = set("cart.items["+itemIndex+"].quantity",newItemQuantity,data);
this.props.client.writeQuery({ ..., data: newData });
It recommend ImmerJS for more complex mutations
Just to save someones time. Using the data in an immutable way was the solution. Agree totally with this answer, but for me I did something else wrong and will show it here. I followed this tutorial and updating the cache worked fine as I finished the tutorial. So I tried to apply the knowledge in my own app, but there the update didn’t work even I did everything similar as showed in the tutorial.
Here was my approach to update the data using the state to access it in the render method:
// ... imports
export const GET_POSTS = gql`
query getPosts {
posts {
id
title
}
}
`
class PostList extends Component {
constructor(props) {
super(props)
this.state = {
posts: props.posts
}
}
render() {
const postItems = this.state.posts.map(item => <PostItem key={item.id} post={item} />)
return (
<div className="post-list">
{postItems}
</div>
)
}
}
const PostListQuery = () => {
return (
<Query query={GET_POSTS}>
{({ loading, error, data }) => {
if (loading) {
return (<div>Loading...</div>)
}
if (error) {
console.error(error)
}
return (<PostList posts={data.posts} />)
}}
</Query>
)
}
export default PostListQuery
The solution was just to access the date directly and not using the state at all. See here:
class PostList extends Component {
render() {
// use posts directly here in render to make `cache.writeQuery` work. Don't set it via state
const { posts } = this.props
const postItems = posts.map(item => <PostItem key={item.id} post={item} />)
return (
<div className="post-list">
{postItems}
</div>
)
}
}
Just for completeness here is the input I used to add a new post and update the cache:
import React, { useState, useRef } from 'react'
import gql from 'graphql-tag'
import { Mutation } from 'react-apollo'
import { GET_POSTS } from './PostList'
const ADD_POST = gql`
mutation ($post: String!) {
insert_posts(objects:{title: $post}) {
affected_rows
returning {
id
title
}
}
}
`
const PostInput = () => {
const input = useRef(null)
const [postInput, setPostInput] = useState('')
const updateCache = (cache, {data}) => {
// Fetch the posts from the cache
const existingPosts = cache.readQuery({
query: GET_POSTS
})
// Add the new post to the cache
const newPost = data.insert_posts.returning[0]
// Use writeQuery to update the cache and update ui
cache.writeQuery({
query: GET_POSTS,
data: {
posts: [
newPost, ...existingPosts.posts
]
}
})
}
const resetInput = () => {
setPostInput('')
input.current.focus()
}
return (
<Mutation mutation={ADD_POST} update={updateCache} onCompleted={resetInput}>
{(addPost, { loading, data }) => {
return (
<form onSubmit={(e) => {
e.preventDefault()
addPost({variables: { post: postInput }})
}}>
<input
value={postInput}
placeholder="Enter a new post"
disabled={loading}
ref={input}
onChange={e => (setPostInput(e.target.value))}
/>
</form>
)
}}
</Mutation>
)
}
export default PostInput
Related
I have a basic job board application. An API is called within the redux store (using thunk function) and initial job results are then saved in redux store.
Ref: https://redux.js.org/tutorials/essentials/part-5-async-logic
These initial Jobs are stored in redux store (and not in local component state), as I need to access these initial job results in other components as well
There are also three filters that can be applied to these initial jobs (Jobs can be filtered by location, team and commitment) I've put these filters inside the redux store as well. (Actions are triggered from
Filter UI component to update the current applied filters, and multiple filters can be active at one time)
The Filter UI component pretty much just renders a <Select> element with a handleChange function which causes the filters to update in the redux store, something like this:
Basic Filter UI Component which dispatches action :
<Select
name={name}
value={value}
onChange={handleChange}
></Select>
// ... omit some code ...
const handleChange = (event) => {
const { name } = event.target;
switch (name) {
case 'location':
dispatch(changeLocationFilter(event.target))
break;
case 'team':
dispatch(changeTeamFilter(event.target))
break;
case 'commitment':
dispatch(changeCommitmentFilter(event.target))
break;
}
}
Here is my filtersSlice in redux, which update the redux state when filters are applied:
import { createSlice } from "#reduxjs/toolkit";
import { ALL_LOCATIONS, ALL_TEAMS, ALL_COMMITMENTS } from '../constants'
const initialState = {
location: ALL_LOCATIONS,
team: ALL_TEAMS,
commitment: ALL_COMMITMENTS
};
export const filtersSlice = createSlice({
name: "filters",
initialState,
reducers: {
changeLocationFilter: (state, action) => {
const { payload: { value: locationValue } } = action;
state.location = locationValue;
},
changeTeamFilter: (state, action) => {
const { payload: { value: teamValue } } = action;
state.team = teamValue;
},
changeCommitmentFilter: (state, action) => {
const { payload: { value: commitmentValue } } = action;
state.commitment = commitmentValue;
}
}
});
// Action creators are generated for each case reducer function
export const { changeLocationFilter, changeTeamFilter, changeCommitmentFilter } = filtersSlice.actions;
export default filtersSlice.reducer;
Every time those filters change, I'm using a memoized createSelector function to get those updated filters, then I'm filtering my jobs locally within my JobContainer component
Ref:
https://redux.js.org/tutorials/essentials/part-6-performance-normalization
Ref:
https://redux-toolkit.js.org/api/createSelector
I am not updating the jobs in the redux store (From initial jobs to filtered jobs) because after doing some reading, it seems that when it comes to filtering data, the generally accepted best practice is to do this via derived state, and there is no need to put this inside component state or redux store state -
Ref:
What is the best way to filter data in React?
Here is some code to illustrate my example further:
Here is my JobsContainer component, which get the initial jobs and the filters from the redux store, and then filters the jobs locally:
import React, { useState, useEffect } from "react";
import { useDispatch, useSelector } from "react-redux";
import { createSelector } from "reselect";
import Job from "../../components/Job";
import { ALL_LOCATIONS, ALL_TEAMS, ALL_COMMITMENTS } from '../../constants'
import { fetchReduxJobs, selectAllReduxJobs } from '../../redux/reduxJobs'
const JobsContainer = () => {
const dispatch = useDispatch()
const reduxJobsStatus = useSelector(state => state.reduxJobs.status);
let reduxJobs = useSelector(selectAllReduxJobs); // GET INITIAL JOBS FROM REDUX STATE HERE
const filterState = useSelector((state) => state.filters); // GET FILTERS FROM REDUX STATE HERE
const selectLocation = filterState => filterState.location
const selectTeam = filterState => filterState.team
const selectCommitment = filterState => filterState.commitment
// CREATE MEMOIZED FUNCTION USING CREATESELECTOR, AND RUN A FILTER ON THE JOBS
// WHENEVER FILTERS CHANGE IN REDUX STORE
const selectFilters = createSelector([selectLocation, selectTeam, selectCommitment], (location, team, commitment) => {
let tempReduxJobs = reduxJobs;
tempReduxJobs = tempReduxJobs.filter((filteredJob) => {
return (
(location === ALL_LOCATIONS ? filteredJob : filteredJob.categories.location === location) &&
(commitment === ALL_COMMITMENTS ? filteredJob : filteredJob.categories.commitment === commitment) &&
(team === ALL_TEAMS ? filteredJob : filteredJob.categories.team === team)
)
})
return tempReduxJobs;
})
reduxJobs = selectFilters(filterState); // UPDATE JOBS HERE WHEN FILTERS CHANGE
let content;
if (reduxJobsStatus === 'loading') {
content = "Loading..."
} else if (reduxJobsStatus === 'succeeded') {
// JUST MODIFYING MY JOBS A BIT HERE BEFORE RENDERING THEM
let groupedReduxJobs = reduxJobs.reduce(function (groupedObj, job) {
const { categories: { team } } = job;
if (!groupedObj[team]) {
groupedObj[team] = []
}
groupedObj[team].push(job)
return groupedObj
}, {})
// THIS IS HOW I RENDER MY JOBS HERE AFTER MODIFYING THEM
content = Object.keys(groupedReduxJobs).map((teamName, index) => (
<div key={index}>
<div className="job-team-heading">{teamName}</div>
{groupedReduxJobs[teamName].map((job) =>
(<Job jobDetails={job} key={job.id} />))
}
</div>
))
// return groupedObj
} else if (reduxJobsStatus === 'failed') {
content = <div>{error}</div>
}
useEffect(() => {
if (reduxJobsStatus === 'idle') {
dispatch(fetchReduxJobs())
}
}, [reduxJobsStatus, dispatch])
return (
<JobsContainerStyles>
<div>{content}</div>
</JobsContainerStyles>
);
}
export default JobsContainer;
Something about how Im updating my jobs after the filters change (inside JobsContainer) using my selectFilters function ie the line:
reduxJobs = selectFilters(filterState);
Seems off. (Note: as you can see, I am modifying the data a bit before rendering as well - see groupedReduxJobs)
I wouldn't be as confused if I was to update the redux store with the filtered jobs after the filter is applied, but as I mentioned, reading into this topic suggests filtered data should generally be kept as derived state, and not in redux store. This is what I am confused about.
Can someone provide some constructive criticism on how I'm doing this please ? Or is the way Im doing this currently a good way to go about solving this problem.
To clarify, this is all working as written here .. but I'm not sure what other's opinions are on doing it this way vs some other way
I'm using Reactjs and Relay. I want to create something if "this" item is not created yet / array is empty. somehow, the relay query returns an empty array if the data has not been loaded, therefore it keeps created "this item" since it's considered empty. how do we determine if relay query data is actually empty or not loaded / pending?
i.e query:
QueryRenderer(MyComponent, {
query: graphql`
query MyComponentQuery($id: ID!) {
items(containerId: $id) {
id
title
}
}
`,
vars: ({ containerId }) => ({
id: containerId
})
})
handle create "this" item:
useEffect(() => {
if(!props.items){
// Create "this" item
}
}, [props.items]);
I am guessing from your snippet that QueryRenderer is an HOC you are using wrapping relay's QueryRender component?
If so, this is how you normally determine if a query is in loading state:
// Example.js
import React from 'react';
import { QueryRenderer, graphql } from 'react-relay';
const renderQuery = ({error, props}) => {
if (error) {
return <div>{error.message}</div>;
} else if (props) {
return <div>{props.page.name} is great!</div>;
}
return <div>Loading</div>;
}
const Example = (props) => {
return (
<QueryRenderer
environment={environment}
query={graphql`
query ExampleQuery($pageID: ID!) {
page(id: $pageID) {
name
}
}
`}
variables={{
pageID: '110798995619330',
}}
render={renderQuery}
/>
);
}
This snippet is taken from the relay.dev docs.
Notice how the render renderProp is getting the props field once the query is no longer loading.
If you are using the new hooks-api, it will be different based on which of the query-hooks you are using.
in my EventForm i have this const, this is a dialog form
this is my EventForm.js
const EventForm = (props) => {
const { setOpenPopup, records, setRecords, setMessage, setOpenSnackbar } = props
const addEvent = () => {
axios.post('https://jsonplaceholder.typicode.com/events', (event)
.then(resp => {
console.log(resp.data)
const newData = [{
title: resp.data.name,
start: resp.data.starts_at,
end: resp.data.ends_at
}]
setRecords([{ ...records, newData}])
//
setOpenPopup(false)
setMessage('New Event added')
setOpenSnackbar(true)
})
.catch([])
}
export default EventForm
EventForm.propTypes = {
setOpenPopup: PropTypes.func,
records: PropTypes.array,
setRecords: PropTypes.func,
setMessage: PropTypes.func,
setOpenSnackbar: PropTypes.func
}
}
in my EventTable.js
const [records, setRecords] = useState([]);
useEffect(() => {
axios.get('https://jsonplaceholder.typicode.com/events')
.then(resp => {
const newData = resp.data.map((item) => ({
title: item.name,
start: item.starts_at,
end: item.ends_at
}))
setRecords(newData)
})
.catch(resp => console.log(resp))
}, [])
fullcalendar...
events={records}
im trying to push the API post response to my setRecords. so when the dialog form close it will not use the GET response. ill just get the new record and render to my view
but im getting an error:
Unhanded Rejection (TypeError): setRecords is not a function
I suspect you are using React Hooks. Make sure that your records state looks like this
const [records, setRecords] = useState([]);
In your axios request, it looks like that you are trying to spread the values of records which is an array to an object. I'd suggest refactoring this to something like this. Instead of trying to spread an array into the object, take the previous state and merge it with the new one.
setRecords(prevRecords => [...prevRecords, ...newData])
Here's an example using React Hooks how the component could look like
import React from "react";
import axios from "axios";
const MyComponent = ({
setOpenPopup,
records,
setRecords,
setMessage,
setOpenSnackbar
}) => {
const addEvent = () => {
axios
.post("https://jsonplaceholder.typicode.com/events", event) // Make sure this is defined somewhere
.then((resp) => {
const { name, starts_at, ends_at } = resp.data;
const newData = [
{
title: name,
start: starts_at,
end: ends_at
}
];
setRecords((prevRecords) => [...prevRecords, ...newData]);
setOpenPopup(false);
setMessage("New Event added");
setOpenSnackbar(true);
})
.catch([]);
};
return (
<div>
<button onClick={addEvent}>Click me </button>
</div>
);
};
export default MyComponent;
If you are not using React Hooks and use Class components, then make sure that you pass setRecords to your component in props. Plus, in your props destructuring, make sure you add this to the props, otherwise, it can lead to unwanted behaviour. Also, move your request function out of the render method and destructure values from the props that you need inside the function. I've also noticed that your axios syntax was incorrect (forgot to close after the event) so I fixed that as well. Here's an example of how you can improve it.
import React from "react";
import axios from "axios";
class MyComponent extends React.Component {
addEvent = () => {
const {
setOpenPopup,
setRecords,
setMessage,
setOpenSnackbar
} = this.props;
axios
.post("https://jsonplaceholder.typicode.com/events", event)
.then((resp) => {
console.log(resp.data);
const newData = [
{
title: resp.data.name,
start: resp.data.starts_at,
end: resp.data.ends_at
}
];
setRecords((prevRecords) => [...prevRecords, ...newData]);
//
setOpenPopup(false);
setMessage("New Event added");
setOpenSnackbar(true);
})
.catch([]);
};
render() {
return (
<div>
<button onClick={() => this.addEvent()}>Click me</button>
</div>
);
}
}
export default MyComponent;
Beginner here.
Trying to fetch some data from a server and display it in my react component once its fetched.
However, I am having trouble integrating the async function into my react component.
import React, { useState } from "react";
import { request } from "graphql-request";
async function fetchData() {
const endpoint = "https://localhost:3090/graphql"
const query = `
query getItems($id: ID) {
item(id: $id) {
title
}
}
`;
const variables = {
id: "123123123"
};
const data = await request(endpoint, query, variables);
// console.log(JSON.stringify(data, undefined, 2));
return data;
}
const TestingGraphQL = () => {
const data = fetchData().catch((error) => console.error(error));
return (
<div>
{data.item.title}
</div>
);
};
export default TestingGraphQL;
I'd like to simply show a spinner or something while waiting, but I tried this & it seems because a promise is returned I cannot do this.
Here you would need to use the useEffect hook to call the API.
The data returned from the API, I am storing here in a state, as well as a loading state to indicate when the call is being made.
Follow along the comments added in between the code below -
CODE
import React, { useState, useEffect } from "react"; // importing useEffect here
import Layout from "#layouts/default";
import ContentContainer from "#components/ContentContainer";
import { request } from "graphql-request";
async function fetchData() {
const endpoint = "https://localhost:3090/graphql"
const query = `
query getItems($id: ID) {
item(id: $id) {
title
}
}
`;
const variables = {
id: "123123123"
};
const data = await request(endpoint, query, variables);
// console.log(JSON.stringify(data, undefined, 2));
return data;
}
const TestingGraphQL = () => {
const [data, setData] = useState(null);
const [loading, setLoading] = useState(true);
// useEffect with an empty dependency array works the same way as componentDidMount
useEffect(async () => {
try {
// set loading to true before calling API
setLoading(true);
const data = await fetchData();
setData(data);
// switch loading to false after fetch is complete
setLoading(false);
} catch (error) {
// add error handling here
setLoading(false);
console.log(error);
}
}, []);
// return a Spinner when loading is true
if(loading) return (
<span>Loading</span>
);
// data will be null when fetch call fails
if (!data) return (
<span>Data not available</span>
);
// when data is available, title is shown
return (
<Layout>
{data.item.title}
</Layout>
);
};
since fetchData() returns a promise you need to handle it in TestingGraphQL. I recommend onComponentMount do your data call. Setting the data retrieved into the state var, for react to keep track of and re-rendering when your data call is finished.
I added a loading state var. If loading is true, then it shows 'loading' otherwise it shows the data. You can go about changing those to components later to suit your needs.
See the example below, switched from hooks to a class, but you should be able to make it work! :)
class TestingGraphQL extends Component {
constructor() {
super();
this.state = { data: {}, loading: true};
}
//when the component is added to the screen. fetch data
componentDidMount() {
fetchData()
.then(json => { this.setState({ data: json, loading: false }) })
.catch(error => console.error(error));
}
render() {
return (
{this.state.loading ? <div>Loading Spinner here</div> : <div>{this.state.data.item.title}</div>}
);
}
};
As shown below I'm getting my data in my nextJS application in the pages/article.js using a graphQL query.
This data is passed down to another react component, which gives me a list of checkboxes.
Selecting a checkbox is calling a mutation to store the ID of the selected checkboxes in the DB.
To get the content updated, I'm using refetchQueries to call the main query again, which will pass the data down to the current component.
So far everything is working. Now I would like to get this stuff realtime using optimistic UI - which makes me some problems...
Replacing the refetchQueries with
update: (store, { data: { getArticle } }) => {
const data = store.readQuery({
query: getArticle,
variables: {
id: mainID
}
})
console.log(data)
}
runs me to the error TypeError: Cannot read property 'kind' of undefined which comes from readQuery.
I don't see what I'm doing wrong. And this is just the first part to get optimisic UI..
pages/article.js
import Article from '../components/Article'
class ArticlePage extends Component {
static async getInitialProps (context, apolloClient) {
const { query: { id }, req } = context
const initProps = { }
// ...
return { id, ...initProps }
}
render () {
const { id, data } = this.props
const { list } = data
return (
<Article
mainID={id}
list={list}
/>
)
}
}
export default compose(
withData,
graphql(getArticle, {
options: props => ({
variables: {
id: props.id
}
})
})
)(ExtendedArticlePage)
components/Article.js
import { getArticle } from '../graphql/article'
import { selectMutation } from '../graphql/selection'
export class Article extends Component {
checkboxToggle (id) {
const { mainID, checkboxSelect } = this.props
checkboxSelect({
variables: {
id
},
refetchQueries: [{
query: getArticle,
variables: {
id: mainID
}
}],
})
}
render () {
const { list } = this.props
return (
list.map(l => {
return (<Checkbox onClick={this.checkboxToggle.bind(this, l.id)} label={l.content} />)
}
)
}
}
export default compose(
graphql(selectMutation, { name: 'checkboxSelect' })
)(Article)
You have a variable shadowing issue in your update code, it seems that you're using the same name getArticle for both your query and the mutation result nested in data.
This is why your call to readQuery fails, the query params you need to provide resolves to the mutation result and not the actual query, hence the TypeError: Cannot read property 'kind' of undefined.
You just need to name your query with another identifier like getQueryArticle.