So I have been trying to use apollo-boost in a React app to use the cache to manage my client state using #client directives on queries but I have been having some issues.
Basically I'm using writeQuery() to write a boolean to my local app state in a Component (let's call it component A) and want to get that value in another Component (let's call it component B) using readQuery() inside the componentDidUpdate method. The thing is, readQuery() in Component B is running before writeQuery in Component A sets the value in the cache/local state so the value read by Component B comes out wrong.
I've confirmed this by using setTimeout to delay the readQuery() and indeed after using the timeout, the value is correct, but this solution can't be trusted, I'm probably not aware of something in Apollo Client because this functionality is pretty basic for local state management. Any Tips?
I believe that in Redux this is solved because the state is being injected to props, which makes the component update, so being that Component A is the one that changes the state, component B wouldn't even have to use componentDidUpdate to get the new value, since the state would be injected and Component B would get updated with the correct value.
Any help would be appreciated, sorry if I didn't make myself clear!
EDIT: The writeQuery() is being used inside a Mutation resolver.
Methods like readQuery and writeQuery are meant to be used to read and modify the cache inside of mutations. In general, they should not be used inside of components directly. By calling readQuery, you are only fetching the data from the cache once. Instead, you should utilize a Query component.
const TODO_QUERY = gql`
query GetTodos {
todos #client {
id
text
completed
}
}
`
<Query query={TODO_QUERY}>
{({ data }) => {
if (data.todos) return <ToDoListComponent todos={data.todos}/>
return null
}}
</Query>
The Query component subscribes to relevant changes to the cache, so the value of data will update when your cache does.
Similarly, you should create appropriate mutations for whatever changes to the cache you're going to make, and then utilize a Mutation component to actually mutate the cache.
const client = new ApolloClient({
clientState: {
defaults: {
todos: []
},
resolvers: {
Mutation: {
addTodo: (_, { text }, { cache }) => {
const previous = cache.readQuery({ query: TODO_QUERY })
const newTodo = { id: nextTodoId++, text, completed: false, __typename: 'TodoItem' }
const data = {
todos: previous.todos.concat([newTodo]),
}
cache.writeQuery({ query, data })
return newTodo
},
},
}
}
})
<Mutation mutation={ADD_TODO}>
{(addTodo) => (
// use addTodo to mutate the cache asynchronously
)}
</Mutation>
Please review the docs for more details.
Related
I would like to have real-time connection with firebase firestore database and update UI after I add or remove some data from collection. I already tried using onSnapshot function but the thing is, when I go to a diffrent route and load other component, all data that was previously in database get change.type === "added" again even if they existed before. So when I get this data its repeating in the context and I'm getting doubled data on the screen, also map function throws an error bcs of redundant key attribute value. What should I do to avoid this? I'm using context hook API + reducers and action creators, no redux!
export const getProjects = (dispatch) => {
firestore.collection('projects').onSnapshot(snapshot => {
snapshot.docChanges().forEach(change => {
if(change.type === "added"){
dispatch({type:'SET_PROJECTS', data: {
title: change.doc.data().title,
content: change.doc.data().content,
id: change.doc.id
}})
}
})
})
}
export const projectReducer = (state, action) => {
switch(action.type){
case 'CREATE_PROJECT_ERR':
console.log('Create project error', action.err.message);
return state
case 'SET_PROJECTS':
return [...state,{
title: action.data.title,
content: action.data.content,
id: action.data.id
}]
default:
return state
}
}
useEffect(() => {
getProjects(dispatch);
}, [dispatch])
That's the code that connects with firestore and get data and also reducer that set's it into the context
onSnapshot always calls the dataCallback at least once, and then again on any changes. Your reducer is simply appending the results to the existing state, so that likely explains why you see it twice - you need to be a tad more subtle to replace existing entries.
I usually work with Redux (rather than Context), but I have a number of instances where my Listeners update Redux state, and the existing Redux connectors update React. And yes,my reducers take care of duplicates.
I have a react functional component that shows list of tags and posts + few static text/decorations. I store the currently selected tag in a state using useState hook. Posts are fetched by using apollo's useQuery hook with tag variable. User should able to select a tag and it will replace the current tag state - thus the useQuery(POSTS_QUERY) will re-run with new tag variable.
const onTagSelectChange = (window: Window,
router: NextRouter,
name: string,
checked: boolean,
tagSetter: React.Dispatch<React.SetStateAction<string>>) => {
if (checked) {
setTagQueryInUrl(window, router, name)
tagSetter(name)
} else {
setTagQueryInUrl(window, router, null)
tagSetter(null)
}
}
const NewsList: NextPage = () => {
const router = useRouter()
const query = router.query as Query
// store tag in state
// initialize tag from `tag` query
const [tag, setTag] = useState(query.tag)
const { data: postsData, loading: postsLoading, error: postsError } = useQuery(
POSTS_QUERY,
{
variables: {
tag: tag
}
}
)
const { data: tagsData, loading: tagsLoading, error: tagsError } = useQuery(TAGS_QUERY)
// show error page if either posts or tags query returned error
if (postsError || tagsError) {
return <Error statusCode={500} />
}
return (
<div>
<h1>Here we have list of news, and I should not re-render everytim :(</h1>
<Tags
loading={tagsLoading}
data={tagsData}
isChecked={(name) => name === tag}
onChange={(name, checked) => onTagSelectChange(window, router, name, checked, setTag)}
/>
<Posts loading={postsLoading} data={postsData} />
</div>
)
}
My question is, why is my h1 block keeps re-rendering even though I don't pass anything to it? Or do I completely misunderstand how react works?
React components re-render whenever their state or props change. If I am reading this correctly then you are changing tag in state whenever the url changes and thus making the component to re-render itself.
As your state is declared on your NewsList component, any state change (as another user stated on his answer) will trigger a re-render of the whole component (NewList) and not only to the components that you have passed your state (thus to the static <h1> you have in there).
If there are parts of this component that have nothing to do with this state, you can move them outside to avoid the re-render.
Though, on cases like this, re-rendering your <h1> is not a cost for React. You should worry and follow this approach on custom components where more complex things going on (e.g. populating lists or calculating stuff etc..). In those cases, you don't want all this complex stuff to happen again, if they are not affected by a parent's state change. You should also always consider, if moving the component outside makes sense or by doing so, you make your code complex.
You should always strike a balance between well-organized and efficient code.
When I use flux-like like Vuex or Redux. I have a question about list state update.
I have list data in flux state.
// state.js
{
itemById: {},
}
And I have item's id array in list component.
// listComponent.js
{
data: {
itemIds: [],
items: data.itemIds.map(id => state.itemById[id]),
};
}
Now I add an item in other component.
// otherComponent.js
addItem(newItem) {
store.dispatch(newItem);
}
Then the itemById state have the newItem, but the itemIds don't have the newItem's id.
if move the itemIds to state. When the listComponent destroy and recreat, itemIds still exist, but I don't want this.
What should I do to update itemIds simply?
Complementing #blaz answer, If you are using redux, in your listComponent.js you must use the connect function from react-redux lib. It will give the possibility to load the data from your state and inject it as props in your component.
const mapStateToProps = (state) => {
const itemIds = Object.keys(state.itemById);
const items = Object.values(state.itemById);
return { itemIds, items };
};
export default connect(mapStateToProps)(ListComponent);
One problem with your code is that you have 2 copies of the same data: one in state and one in data of listComponent. Assuming listComponent's data is initiated from state's data, for every update in state, you need to catch and update local component's data to match them. Destroying and recreating the component does not directly update local data, and it's not recommended to recreate the whole component just for the local data, since it beats the purpose of component life cycle.
Flux aside, one of the reasons people use state management tool like redux or vuex is because of single source of truth - there should be one copy of global state from which any component can read. To fix your code, one solution is to move itemIds and items from data to computed and directly take them from state.
computed: {
itemIds() {
return Object.keys(state.itemByIds);
},
items() {
return Object.values(state.itemByIds);
}
}
So there is this simplified structure in my app:
In component i have :
handlePageClick(data) {
this.props.onChangePage(data.selected + 1);
this.props.onSearchSomething(this.props.PM);
}
In my Container :
const mapDispatchToProps = dispatch => ({
onUpdateForm: (propPath, val) => {
dispatch(updatePMForm(propPath, val));
},
onSearchSomething: (payload) => {
// TODO: process the PM state here when integrating with the server in order to extract
// the proper payload out of the state object
dispatch(searchSomething(15, payload));
},
onChangePage: (pageNumber) => {
dispatch(changePage(pageNumber));
},
});
So when i trigger pageClick, and onChangePage returns newState, in mapDispatchToProps dispatch(searchSomething(15, payload)) receive previous State in payload, that hasn't been updated yet.
You should use componentDidUpdate or componentWillUpdate and diff previous state with current one and then call this.props.onSearchSomething with updated value from the store rather than call it one after another in handlePageClick. Why is that? State changes in a component are potentially asynchronous and almost in all cases there's no way that the component's props will be updated between this.props.onChangePage and this.props.onSearchSomething calls. Please read this issue for better understanding what I'm talking about.
But to help you out a bit some time ago I created a small utility map-props-changes-to-callbacks to assign callbacks in your components to specific changes in redux store. Perhaps that can be helpful in your case and you can write your code like that:
handlePageClick(data) {
this.props.onChangePage(data.selected + 1)
}
onPageChanged() {
this.props.onSearchSomething(this.props.PM)
}
Also, you should consider moving your logic to a single action so you can just dispatch one action and get the search result in return.
I'm trying to set up a React app where clicking a map marker in one component re-renders another component on the page with data from the database and changes the URL. It works, sort of, but not well.
I'm having trouble figuring out how getting the state from Redux and getting a response back from the API fit within the React life cycle.
There are two related problems:
FIRST: The commented-out line "//APIManager.get()......" doesn't work, but the hacked-together version on the line below it does.
SECOND: The line where I'm console.log()-ing the response logs infinitely and makes infinite GET requests to my database.
Here's my component below:
class Hike extends Component {
constructor() {
super()
this.state = {
currentHike: {
id: '',
name: '',
review: {},
}
}
}
componentDidUpdate() {
const params = this.props.params
const hack = "/api/hike/" + params
// APIManager.get('/api/hike/', params, (err, response) => { // doesn't work
APIManager.get(hack, null, (err, response) => { // works
if (err) {
console.error(err)
return
}
console.log(JSON.stringify(response.result)) // SECOND
this.setState({
currentHike: response.result
})
})
}
render() {
// Allow for fields to be blank
const name = (this.state.currentHike.name == null) ? null : this.state.currentHike.name
return (
<div>
<p>testing hike component</p>
<p>{this.state.currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
params: state.hike.selectedHike
}
}
export default connect(stateToProps)(Hike)
Also: When I click a link on the page to go to another url, I get the following error:
"Warning: setState(...): Can only update a mounted or mounting component. This usually means you called setState() on an unmounted component. This is a no-op."
Looking at your code, I think I would architect it slightly differently
Few things:
Try to move the API calls and fetch data into a Redux action. Since API fetch is asynchronous, I think it is best to use Redux Thunk
example:
function fetchHikeById(hikeId) {
return dispatch => {
// optional: dispatch an action here to change redux state to loading
dispatch(action.loadingStarted())
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err);
// if you want user to know an error happened.
// you can optionally dispatch action to store
// the error in the redux state.
dispatch(action.fetchError(err));
return;
}
dispatch(action.currentHikeReceived(response.result))
});
}
}
You can map dispatch to props for fetchHikeById also, by treating fetchHikeById like any other action creator.
Since you have a path /hike/:hikeId I assume you are also updating the route. So if you want people to book mark and save and url .../hike/2 or go back to it. You can still put the the fetch in the Hike component.
The lifecycle method you put the fetchHikeById action is.
componentDidMount() {
// assume you are using react router to pass the hikeId
// from the url '/hike/:hikeId'
const hikeId = this.props.params.hikeId;
this.props.fetchHikeById(hikeId);
}
componentWillReceiveProps(nextProps) {
// so this is when the props changed.
// so if the hikeId change, you'd have to re-fetch.
if (this.props.params.hikeId !== nextProps.params.hikeId) {
this.props.fetchHikeById(nextProps.params.hikeId)
}
}
I don't see any Redux being used at all in your code. If you plan on using Redux, you should move all that API logic into an action creator and store the API responses in your Redux Store. I understand you're quickly prototyping now. :)
Your infinite loop is caused because you chose the wrong lifecycle method. If you use the componentDidUpdate and setState, it will again cause the componentDidUpdatemethod to be called and so on. You're basically updating whenever the component is updated, if that makes any sense. :D
You could always check, before sending the API call, if the new props.params you have are different than the ones you previously had (which caused the API call). You receive the old props and state as arguments to that function.
https://facebook.github.io/react/docs/react-component.html#componentdidupdate
However, if you've decided to use Redux, I would probably move that logic to an action creator, store that response in your Redux Store and simply use that data in your connect.
The FIRST problem I cannot help with, as I do not know what this APIManager's arguments should be.
The SECOND problem is a result of you doing API requests in "componentDidUpdate()". This is essentially what happens:
Some state changes in redux.
Hike receives new props (or its state changes).
Hike renders according to the new props.
Hike has now been updated and calls your "componentDidUpdate" function.
componentDidUpdate makes the API call, and when the response comes back, it triggers setState().
Inner state of Hike is changed, which triggers an update of the component(!) -> goto step 2.
When you click on a link to another page, the infinite loop is continued and after the last API call triggered by an update of Hike is resolved, you call "setState" again, which now tries to update the state of a no-longer-mounted component, hence the warning.
The docs explain this really well I find, I would give those a thorough read.
Try making the API call in componentDidMount:
componentDidMount() {
// make your API call and then call .setState
}
Do that instead of inside of componentDidUpdate.
There are many ways to architect your API calls inside of your React app. For example, take a look at this article: React AJAX Best Practices. In case the link is broken, it outlines a few ideas:
Root Component
This is the simplest approach so it's great for prototypes and small apps.
With this approach, you build a single root/parent component that issues all your AJAX requests. The root component stores the AJAX response data in it's state, and passes that state (or a portion of it) down to child components as props.
As this is outside the scope of the question, I'll leave you to to a bit of research, but some other methods for managing state and async API calls involved libraries like Redux which is one of the de-facto state managers for React right now.
By the way, your infinite calls come from the fact that when your component updates, it's making an API call and then calling setState which updates the component again, throwing you into an infinite loop.
Still figuring out the flow of Redux because it solved the problem when I moved the API request from the Hike component to the one it was listening to.
Now the Hike component is just listening and re-rendering once the database info catches up with the re-routing and re-rendering.
Hike.js
class Hike extends Component {
constructor() {
super()
this.state = {}
}
componentDidUpdate() {
console.log('dealing with ' + JSON.stringify(this.props.currentHike))
}
render() {
if (this.props.currentHike == null || undefined) { return false }
const currentHike = this.props.currentHike
return (
<div className="sidebar">
<p>{currentHike.name}</p>
</div>
)
}
}
const stateToProps = (state) => {
return {
currentHike: state.hike.currentHike,
}
}
And "this.props.currentHikeReceived()" got moved back to the action doing everything in the other component so I no longer have to worry about the Hikes component infinitely re-rendering itself.
Map.js
onMarkerClick(id) {
const hikeId = id
// Set params to be fetched
this.props.hikeSelected(hikeId)
// GET hike data from database
const hack = "/api/hike/" + hikeId
APIManager.get(hack, null, (err, response) => {
if (err) {
console.error(err)
return
}
this.props.currentHikeReceived(response.result)
})
// Change path to clicked hike
const path = `/hike/${hikeId}`
browserHistory.push(path)
}
const stateToProps = (state) => {
return {
hikes: state.hike.list,
location: state.newHike
}
}
const dispatchToProps = (dispatch) => {
return {
currentHikeReceived: (hike) => dispatch(actions.currentHikeReceived(hike)),
hikesReceived: (hikes) => dispatch(actions.hikesReceived(hikes)),
hikeSelected: (hike) => dispatch(actions.hikeSelected(hike)),
locationAdded: (location) => dispatch(actions.locationAdded(location)),
}
}