Why Relay Modern is making new requests, instead of using cache - reactjs

I have a page where you can see the current Item and click "Next" to see the next one. Here is how this component looks like:
class App extends Component {
constructor(props) {
super(props);
this.state = {
index: 0,
ids: ["VXNlcjox", "VXNlcjoy"]
};
this.onNext = () => this.setState(s => ({ index: (s.index + 1) % 2 }));
}
render() {
const { index, ids } = this.state;
return (
<div>
<button type="button" onClick={this.onNext}>
next
</button>
<QueryRenderer
environment={environment}
query={graphql`
query App_Query($id: ID!) {
node(id: $id) {
id
}
}
`}
variables={{ id: ids[index] }}
render={({ error, props }) => {
if (error) {
return <div>Error!</div>;
}
if (!props) {
return <div>Loading...</div>;
}
return <pre>{JSON.stringify(props, null, 2)}</pre>;
}}
/>
</div>
);
}
}
What I expect, is that each Item will be fetched only when requested for the first time and later used from cache.
But what I see, is that the new request made in the network tab every time I click "next", even if this Item was requested before. If I open Relay DevTools - Items with this id is already in the store:
So why is the new request made every time? Isn't Relay Modern supposed to reuse previously cached data?

You can pass the QueryRenderer one of these dataFrom props:
NETWORK_ONLY – return to the network each time.
STORE_THEN_NETWORK – attempt to render from the store, then update the store with fresh data from the network.
Try this:
<QueryRenderer
dataFrom="STORE_THEN_NETWORK"
...
/>
See the implementation: https://github.com/facebook/relay/blob/master/packages/react-relay/modern/ReactRelayQueryRenderer.js#L297-L299

I don't think it's the way query renderer is meant to work by default.
You're probably looking for this relay-query-lookup-renderer

Related

How to force Apollo Query component to re-run query when parent component re-renders

I'm using Apollo Client's <Query> within a component that is re-rendered when state is changed within a lifecycle method. I wish to have my <Query> component re-run the query because I know that data has changed.
It appears that Query component is using a cache that needs to be invalidated before query is re-run.
I'm using a wonky workaround that caches the refetch callback from the render prop in the parent component, but it feels wrong. I'll post my approach in the answers if anyone is interested.
My code looks something like this. I removed loading and error handling from query as well as some other detail for brevity.
class ParentComponent extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.refetchId !== prevProps.refetchId) {
const otherData = this.processData() // do something
this.setState({otherData}) // this forces component to reload
}
}
render() {
const { otherData } = this.state
return (
<Query query={MY_QUERY}>
{({ data }) => {
return <ChildComponent gqlData={data} stateData={otherData} />
}}
</Query>
)
}
}
How do I force <Query> to fetch new data to pass to <ChildComponent>?
Even though ParentComponent re-renders when props or state change, Query doesn't re-run. ChildComponent gets an updated stateData prop, but has a stale gqlData prop. As I understand Apollo's query cache need to be invalidated, but I'm not sure.
Please note that passing refetch to ChildComponent is not the answer because it only displays information from GraphQL and wouldn't know when to refetch. I don't want to introduce timers or otherwise complicate ChildComponent to solve this - it doesn't need to know about this complexity or data fetching concerns.
I had almost a similar situation which I solved with fetchPolicy attribute:
<Query fetchPolicy="no-cache" ...
The task was to load some details from server by clicking on a list item.
And if I wanted to add an action to force re-fetching the query (such as modifying the details), I first assigned the refetch to this.refetch:
<Query fetchPolicy="no-cache" query={GET_ACCOUNT_DETAILS} variables=... }}>
{({ data: { account }, loading, refetch }) => {
this.refetch = refetch;
...
And in the specific action that I wanted the refetch to happen, I called:
this.refetch();
In addition to the already accepted answer above there are 2 ways of achieving this.
If all you need is getting data from the graphql server to generate a list every time a component is mounted you have these options:
Use the option fetchPolicy="no-cache"
export default defineComponent({
setup() {
const { result, loading, error } = useMyQuery(
() => {
return { date: new Date() }
},
{
pollInterval: 15 * 60 * 1000, // refresh data every 15 min
fetchPolicy: 'no-cache',
}
)
Use the refetch() method while keeping the cache in place:
export default defineComponent({
setup() {
const { result, loading, error}, refetch } = useMyQuery(
() => {
return { date: new Date() }
},
{
pollInterval: 15 * 60 * 1000, // refresh data every 15 min
}
)
if (!loading.value) void refetch()
It seems to me that the Query component doesn't necessarily need to be inside this ParentComponent.
In that case, I would move the Query component up, since I would still be able to render other stuff while I don't have results in the ChildComponent. And then I would have access to the query.refetch method.
Note that in the example I added the graphql hoc, but you can still use Query component around <ParentComponent />.
class ParentComponent extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.refetchId !== prevProps.refetchId) {
const otherData = this.processData() // do something
// won't need this anymore, since refetch will cause the Parent component to rerender)
// this.setState({otherData}) // this forces component to reload
this.props.myQuery.refetch(); // >>> Refetch here!
}
}
render() {
const {
otherData
} = this.state;
return <ChildComponent gqlData={this.props.myQuery} stateData={otherData} />;
}
}
export graphql(MY_QUERY, {
name: 'myQuery'
})(ParentComponent);
Could you refetch in parent component? Once the parent component get an update, then you can evaluate whether to trigger a fetch or not.
I have done it without using Query like the following:
class ParentComp extends React.Component {
lifeCycleHook(e) { //here
// call your query here
this.props.someQuery()
}
render() {
return (
<div>
<Child Comp data={this.props.data.key}> //child would only need to render data
</div>
);
}
}
export default graphql(someQuery)(SongCreate);
So you can trigger your fetch anytime you want it to. You can get the query as a prop in this case.
For your case, you would put your query into a prop using export default graphql(addSongQuery)(SongCreate);. Then call it in your lifecyclehooks DidUpdate.
Another options is to use refetch on Query.
<Query
query={GET_DOG_PHOTO}
variables={{ breed }}
skip={!breed}
>
{({ loading, error, data, refetch }) => {
if (loading) return null;
if (error) return `Error!: ${error}`;
return (
<div>
<img
src={data.dog.displayImage}
style={{ height: 100, width: 100 }}
/>
<button onClick={() => refetch()}>Refetch!</button>
</div>
);
}}
</Query>
The second method would require you pass something down to your child, which isn't really all that bad.
As promised, here's my hacky workaround
class ParentComponent extends React.Component {
componentDidUpdate(prevProps) {
if (this.props.refetchId !== prevProps.refetchId) {
const otherData = this.processData() // do something
this.setState({otherData})
if (this.refetch) {
this.refetch()
}
}
}
render() {
const { otherData } = this.state
const setRefetch = refetch => {this.refetch = refetch}
return (
<Query query={MY_QUERY}>
{({ data, refetch }) => {
setRefetch(refetch)
return <ChildComponent gqlData={data} stateData={otherData} />
}}
</Query>
)
}
}

Handling responded forms in react

I'm doing a simple project that has something like 3 forms and right now I start the component with empty Inputs and then request data from API to pre-populate the form using the componentWillMount() hook.
It works for me now but if someday my app need more and more data it would be annoying to do this everytime for any new form and I would like to know if there is any lib or pattern to help pre-populating forms without using any state container (Redux, mobx, and I really don't know if they are needed in this case).
It is better to do your data fetching in componentDidMount than in componentWillMount:
If you need to load data from a remote endpoint, this is a good place
to instantiate the network request.
If you want to reuse some data fetching logic without any external state you could use Component with render props or Higher Order Components.
For example:
function withData(fetchData) {
return BaseComponent => {
class WithData extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
}
}
componentDidMount(){
fetchData().then(response => {
this.setState({ data: response })
})
}
render(){
return <BaseComponent {...this.props} data={this.state.data} />
}
}
return WithData;
}
}
And later you can reuse this logic:
const DataList = ({ data }) => (
<ul>
{
data.map(item =>
<li>{item.name}</li>
)
}
</ul>
)
// passing Promises as a `data` producers
const UserDataList = withData(fetchUsers)(DataList);
const GroupDataList = withData(fetchGroups)(DataList);
const CatsDataList = withData(() => fetchAnimals('cats'))(DataList);
const ListOfEverything = () => (
<Container>
<UserDataList />
<GroupDataList />
<CatsDataList />
</Container>
)

What is best approach to set data to component from API in React JS

We have product detail page which contains multiple component in single page.
Product Component looks like:
class Product extends Component {
render() {
return (
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details/>
<Contact/>
<SimilarProd/>
<OtherProd/>
</div>
);
}
}
Here we have 3 APIs for
- Details
- Similar Product
- Other Products
Now from Detail API we need to set data to these components
<Gallery/>
<Video/>
<Details/>
<Contact/>
In which component we need to make a call to API and how to set data to other components. Lets say we need to assign a,b,c,d value to each component
componentWillMount(props) {
fetch('/deatail.json').then(response => {
if (response.ok) {
return response.json();
} else {
throw new Error('Something went wrong ...');
}
})
.then(data => this.setState({ data, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
OR
Do we need to create separate api for each components?
Since it's three different components you need to make the call in the component where all the components meet. And pass down the state from the parent component to child components. If your app is dynamic then you should use "Redux" or "MobX" for state management. I personally advise you to use Redux
class ParentComponent extends React.PureComponent {
constructor (props) {
super(props);
this.state = {
gallery: '',
similarPdts: '',
otherPdts: ''
}
}
componentWillMount () {
//make api call and set data
}
render () {
//render your all components
}
}
The Product component is the best place to place your API call because it's the common ancestor for all the components that need that data.
I'd recommend that you move the actual call out of the component, and into a common place with all API calls.
Anyways, something like this is what you're looking for:
import React from "react";
import { render } from "react-dom";
import {
SearchBar,
Gallery,
Video,
Details,
Contact,
SimilarProd,
OtherProd
} from "./components/components";
class Product extends React.Component {
constructor(props) {
super(props);
// Set default values for state
this.state = {
data: {
a: 1,
b: 2,
c: 3,
d: 4
},
error: null,
isLoading: true
};
}
componentWillMount() {
this.loadData();
}
loadData() {
fetch('/detail.json')
.then(response => {
// if (response.ok) {
// return response.json();
// } else {
// throw new Error('Something went wrong ...');
// }
return Promise.resolve({
a: 5,
b: 6,
c: 7,
d: 8
});
})
.then(data => this.setState({ data, isLoading: false }))
.catch(error => this.setState({ error, isLoading: false }));
}
render() {
if (this.state.error) return <h1>Error</h1>;
if (this.state.isLoading) return <h1>Loading</h1>;
const data = this.state.data;
return (
<div>
<SearchBar/>
<Gallery a={data.a} b={data.b} c={data.c} d={data.d} />
<Video a={data.a} b={data.b} c={data.c} d={data.d} />
<Details a={data.a} b={data.b} c={data.c} d={data.d} />
<Contact a={data.a} b={data.b} c={data.c} d={data.d} />
<SimilarProd/>
<OtherProd/>
</div>
);
}
}
render(<Product />, document.getElementById("root"));
Working example here:
https://codesandbox.io/s/ymj07k6jrv
You API calls will be in the product component. Catering your need to best practices, I want to make sure that you are using an implementation of FLUX architecture for data flow. If not do visit phrontend
You should send you API calls in componentWillMount() having your state a loading indicator that will render a loader till the data is not fetched.
Each of your Components should be watching the state for their respective data. Let say you have a state like {loading:true, galleryData:{}, details:{}, simProducts:{}, otherProducts:{}}. In render the similar products component should render if it finds the respective data in state. What you have to do is to just update the state whenever you receive the data.
Here is the working code snippet:
ProductComponent:
import React from 'react';
import SampleStore from '/storepath/SampleStore';
export default class ParentComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
loading:true,
}
}
componentWillMount () {
//Bind Store or network callback function
this.handleResponse = this.handleResponse
//API call here.
}
handleResponse(response){
// check Response Validity and update state
// if you have multiple APIs so you can have a API request identifier that will tell you which data to expect.
if(response.err){
//retry or show error message
}else{
this.state.loading = false;
//set data here in state either for similar products or other products and just call setState(this.state)
this.state.similarProducts = response.data.simProds;
this.setState(this.state);
}
}
render () {
return(
<div>
{this.state.loading} ? <LoaderComponent/> :
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details/>
<Contact/>
{this.state.similarProducts && <SimilarProd data={this.state.similarProducts}/>}
{this.state.otherProducts && <OtherProd data={this.state.otherProducts}/>}
</div>
</div>
);
}
}
Just keep on setting the data in the state as soon as you are receiving it and render you components should be state aware.
In which component we need to make a call to API and how to set data
to other components.
The API call should be made in the Product component as explained in the other answers.Now for setting up data considering you need to make 3 API calls(Details, Similar Product, Other Products) what you can do is execute the below logic in componentDidMount() :
var apiRequest1 = fetch('/detail.json').then((response) => {
this.setState({detailData: response.json()})
return response.json();
});
var apiRequest2 = fetch('/similarProduct.json').then((response) => { //The endpoint I am just faking it
this.setState({similarProductData: response.json()})
return response.json();
});
var apiRequest3 = fetch('/otherProduct.json').then((response) => { //Same here
this.setState({otherProductData: response.json()})
return response.json();
});
Promise.all([apiRequest1,apiRequest2, apiRequest3]).then((data) => {
console.log(data) //It will be an array of response
//You can set the state here too.
});
Another shorter way will be:
const urls = ['details.json', 'similarProducts.json', 'otherProducts.json'];
// separate function to make code more clear
const grabContent = url => fetch(url).then(res => res.json())
Promise.all(urls.map(grabContent)).then((response) => {
this.setState({detailData: response[0]})
this.setState({similarProductData: response[1]})
this.setState({otherProductData: response[2]})
});
And then in your Product render() funtion you can pass the API data as
class Product extends Component {
render() {
return (
<div>
<Searchbar/>
<Gallery/>
<Video/>
<Details details={this.state.detailData}/>
<Contact/>
<SimilarProd similar={this.state.similarProductData}/>
<OtherProd other={this.state.otherProductData}/>
</div>
);
}
}
And in the respective component you can access the data as :
this.props.details //Considering in details component.

How to log exackly one object from fetched json data in react?

Is there a way to log 1 object or 2 or as much as i want to be logged in console?
Im using simple data from jsonplaceholder.typicode.com (quite usefull for learning purpose) which every of object has unique id.
For example:
I fetched data and rendered 200 posts on website, ok... but if i have a data which contains 100 logos or banners or layouts for website i want to render a specific logo or banner with unique id 30.
So, how can I render only 1st, 2nd, 3rd or x post/posts from 200?
This is what i have now:
App.js
import React, { Component } from 'react';
import './css/App.css';
import './css/bootstrap.css';
import $ from 'jquery';
import Todos from './Todos';
class App extends Component {
constructor(props) {
super(props);
this.state={
todos:[],
};
}
getTodos() {
$.ajax({
url: 'https://jsonplaceholder.typicode.com/todos',
dataType: 'json',
cache: false,
success: function (data) {
this.setState({todos: data}, function () {
console.log(this.state);
});
}.bind(this),
error: function (xhr, status, err) {
console.log(err);
}
})
}
componentWillMount(){
this.getTodos();
}
componentDidMount() {
this.getTodos();
}
render() {
return (
<div>
<Todos todos={this.state.todos}/>
</div>
);
}
}
export default App;
Todos.js
import React, {Component} from 'react';
import TodoItem from './TodoItem';
class Todos extends Component {
render() {
let todoItems;
if(this.props.todos) {
todoItems = this.props.todos.map(todo => {
return (
<TodoItem key={todo.title} todo = {todo} />
);
});
}
return (
<div>
<h2>Todo list from api</h2>
{todoItems}
</div>
);
}
}
Todos.propTypes = {
todos: React.PropTypes.array
};
export default Todos;
TodoItem.js
import React, {Component} from 'react';
class TodoItem extends Component {
render() {
return (
<li>
<strong>{this.props.todo.title}</strong>
</li>
);
}
}
TodoItem.propTypes = {
todo: React.PropTypes.object
};
export default TodoItem;
#edit
Is there possible to filter mapped json data and find object depending on id and then render this object?
Slicing multiple items
You can use another state property, like filterCount, which can be either set by you manually, or you can trigger setState events from buttons e.g.
constructor(props) {
super(props);
this.setFilter = this.setFilter.bind(this);
this.state={
todos: [],
filterCount: 20 // Default value
};
}
setFilter(count) {
this.setState({ filterCount: count });
}
render() {
const { todos, filterCount } = this.state;
return(
<div>
...
<button onClick={this.setFilter(10)} />
<button onClick={this.setFilter(20)} />
<button onClick={this.setFilter(50)} />
</div>
)
}
This will ensure that your component is rerendered each time when you change the count.
Now the second part, filtering the first x items.
The main way is to use Array.prototype.slice
Slice example:
render() {
const { todos, filterCount } = this.state;
const filteredToDos = todos.slice(0,filterCount);
return(
<div>
...
{ filteredToDos.map(todo => {
return (
<TodoItem key={todo.title} todo={todo} />
);
}) }
</div>
)
}
Make sure you don't accidentally use splice instead of slice, because splice doesn't do the copy to a new array, but modifies the original one.
This example can be easily modified to support paging aswell. You could add a currentPage parameter to state, and just modify the slice line to slice the array based on which page you are.
Also, think about hard whether you need to use jQuery in your app. If you are using it only for convenience, then you are making a big mistake, as it is a hefty library that increases your bundle size considerably. Try to learn doing things the react way :)
Your ajax request can be done by using fetch api.
return fetch("https://jsonplaceholder.typicode.com/todos")
.then(response => response.json())
.catch((error) => {
console.error(error);
});
If you don't need out of the box support for the older browsers, fetch api will be fine for last 2-3 major versions of modern browsers, including mobile ones. It can be polyfilled for older ones such as IE8 aswell.
Also, there are libraries like axios that are actually much smaller then jQuery if you need more options and support for all request types and headers.
Also, the fetch action itself, could be decoupled from the component to a separate actions.js file so it can be imported / used from multiple components in the future, or refactored more easily to support working with something like Redux if your app grows.
Getting a single item
Fetching single item from API directly - suggested
If we are talking about performance, then the best way is to get a single item directly from API. Considering that this is a REST based api then the way would be:
https://jsonplaceholder.typicode.com/todos/{id}
Rewriten for a fetch example with a template literal:
return fetch(`https://jsonplaceholder.typicode.com/todos/${itemId}`)
.then(response => response.json())
.catch((error) => {
console.error(error);
});
This should also return a single object, not an array, so you should be able to pass it as a prop and use it immediately. IMHO, this is the best solution to the problem.
Fetching all items from API, then filtering with filter
Second option is to get all layouts, then filter the result which you can see in the example of: #mayank-shukla
Bottleneck of this method is that Array.prototype.filter can return multiple results.
Fetching all items from API, then filtering with find
Third option is to get all layouts, then filter the result with Array.prototype.find
Example is the same as filter, just change the keyword to find.
Find will return the first result in case of multiple ones.
Fetching all items from API, then filtering by index
Fourth option is getting all layouts, then filtering by index, e.g.
todos[itemId-1]
This will also work, but, you need to be 100% certain that backend will order the results in the array by their id. Also, index starts from 0, and your ids from 1, so this can be a source of major confusion.
Instead of passing all the data to child component filter out the data on the basis of condition and pass that filtered data to child component.
Like this:
render() {
//filter data of these ids
let ids = [1,2,3,4];
//filtered data
let data = this.state.todos.filter(el => ids.indexOf(el.id) != -1);
return (
<div>
<Todos todos={data}/>
</div>
);
}
If you want to pass first x data, the use #array.slice and pass the result, like this:
render() {
//filtered data
let data = this.state.todos.slice(0, 20);
return (
<div>
<Todos todos={data}/>
</div>
);
}
Instead of putting the filter part inside render you can do that after getting the response.

How to render a long list in react-relay?

A lazy scrolling list component for react-web
backing onto a Relay Connection, with paginated fetching,
while offering good performance and memory characteristics.
Two things to manage
Data fetching through providing pagination parameters to query, manipulation of the relay store
Component rendering, manipulation of the virtual DOM
Is there some particular list component which handles this well?
Is there an established pattern for implementing this common mechanism?
This pattern is pretty much the representative scenario for connections. Here's a hypothetical <PostsIndex> component that shows a list of posts with a "load more" button. If you don't want to explicitly change the UI when in the isLoading state you could delete the constructor and the setVariables callback. Adding viewport based infinite scrolling would not be hard either; you'd just need to wire a scroll listener up to you setVariables call.
class PostsIndex extends React.Component {
constructor(props) {
super(props);
this.state = {isLoading: false};
}
_handleLoadMore = () => {
this.props.relay.setVariables({
count: this.props.relay.variables.count + 10,
}, ({ready, done, error, aborted}) => {
this.setState({isLoading: !ready && !(done || error || aborted)});
});
}
render() {
return (
<div>
{
this.props.viewer.posts.edges.map(({node}) => (
<Post key={node.id} post={node} />
))
}
{
this.props.viewer.posts.pageInfo.hasNextPage ?
<LoadMoreButton
isLoading={this.state.isLoading}
onLoadMore={this._handleLoadMore}
/> :
null
}
</div>
);
}
}
export default Relay.createContainer(PostsIndex, {
initialVariables: {
count: 10,
},
fragments: {
viewer: () => Relay.QL`
fragment on User {
posts(first: $count) {
edges {
node {
id
${Post.getFragment('post')}
}
}
pageInfo {
hasNextPage
}
}
}
`,
},
});

Resources