Relay.js - partial fetch when conditional statement changes - reactjs

I cannot for the life of me figure this out. I have a parent component that gets a 'layout' field from the database. That value determines which child component should be rendered. The parent component also needs to get a GraphQL fragment from the child component. The parent component looks like this:
class Page extends React.Component{
_setLayout(){
const Layout = this.props.viewer.post.layout.meta_value || 'DefaultLayout';
const isDefault = Layout === 'DefaultLayout';
const isPostList = Layout === 'PostList';
this.props.relay.setVariables({
page: this.props.page,
isDefault: isDefault,
isPostList: isPostList
})
}
componentWillMount(){
this._setLayout()
}
render(){
const { viewer, className } = this.props;
const { post } = viewer;
const Layout = Layouts[post.layout.meta_value] || Layouts['Default'];
return <Layout.Component viewer={viewer} page={this.props.page} condition={true} layout={Layout}/>
}
}
export default Relay.createContainer(Page, {
initialVariables: {
page: null,
isDefault: false,
isPostList: false,
},
fragments: {
viewer: (variables) => Relay.QL`
fragment on User {
${DefaultLayout.getFragment('viewer', {page: variables.page, condition: variables.isDefault})},
post(post_name: $page){
id
layout{
id
meta_value
}
}
}
`
}
});
The child component (in this example DefaultLayout) looks like this:
class DefaultLayout extends React.Component{
componentWillMount(){
this.props.relay.setVariables({
page: this.props.page,
condition: this.props.condition
})
}
render(){
const { viewer } = this.props;
if (viewer.post){
const { post_title, post_content, thumbnail } = viewer.post;
let bg = {
backgroundImage: "url('" + thumbnail + "')"
}
let heroClass = thumbnail ? "hero_thumbnail" : "hero";
return(
<PostContent content={post_content}/>
)
} else {
return <div>Loading...</div>
}
}
}
export default Relay.createContainer(DefaultLayout, {
initialVariables:{
page: null,
condition: false
},
fragments: {
viewer: () => Relay.QL`
fragment on User {
post(post_name:$page) #include(if: $condition){
id
post_title
post_content
thumbnail
},
settings{
id
uploads
amazonS3
}
}
`
}
});
When the initial query is run, condition is false so it doesn't get any post data. Right before DefaultLayout mounts, condition variable is set to true so it SHOULD query post and get that data.
Here is the weird thing: I'm only getting the id and post_title of post. I'm not getting post_content or thumbnail. If I set condition to be true in initial variables, I get everything.
Am I not supposed to be able to change the condition?
Lastly - here is a screenshot of the query that's generating. Its querying Page, not Routes, which seems wrong.

I think the problem is that you've not passed what you've expected to pass to LayoutComponent. Your page variable can be found in Relay's variables store, not on props:
return (
<Layout.Component
condition={true}
layout={Layout}
page={this.props.relay.variables.page}
viewer={viewer}
/>
);
Also, you can omit the componentWillMount step in DefaultLayout. page and condition should flow into variables from props automatically.
See also:
https://github.com/facebook/relay/blob/master/src/container/RelayContainer.js#L790-L807
https://github.com/facebook/relay/issues/309
https://github.com/facebook/relay/issues/866

Related

Accessing a property that has an object as it's value

I'm working with an API that retrieves data from a request from a user. The data returned looks something like
{
name: "mewtwo"
id: 150
types: Array(1)
0:
slot: 1
type: {
name: "psychic", url: "https://pokeapi.co/api/v2/type/14"
}
}
I want to access the "psychic" value in type but I receive an undefined for "type". I'm able to display the name property and id just fine I just can't get the "psychic" value.
const Display = (props) => {
console.log(props.data.types);
return (
<>
<h1 className="pokemon-name">{props.data.name}</h1>
<p>{props.data.id}</p>
<p>{props.data.types.type.name}</p>//Will throw an error here
</>
);
}
You are trying to access an array element. Change it to the following
props.data.types[0].type.name
const Display = (props) => {
let { data } = props;
return (
<>
<h1 className="pokemon-name">{data.name}</h1>
<p>{data.id}</p>
<p>{data.types[0].type.name}</p>
</>
);
}
Since data.types is an Array of Objects, I think you want to access the first entry in the data.types array.
So I'd replace {props.data.types.type.name} with {props.data.types[0].type.name}.
For safety reasons I'd check for the existence of that array and extract the data out of it before using it like so:
const Display = ({data}) => {
// destructure properties out of data prop
const { name, id, types } = data;
// extract type name
const typeName = types[0].name;
return (
<>
<h1 className="pokemon-name">{name}</h1>
<p>{id}</p>
<p>{typeName}</p>
</>
);
}
Taking in account that your data is coming from an API, the types prop may not be populated when you're trying to access it. We can account for that scenario like so:
Here's a wrapper component example. this component gets the async data and renders the Display component.
// import as you would to make React work.
class Wrapper extends Component {
constructor(props) {
super(props);
this.state = {
dataFromApi: false
}
}
componentDidMount() {
// async function to get data from api (ex: fetch)
fetchDataFromAPI().then(res => res.json()).then(data => {
this.setState({ dataFromApi: data })
});
}
render() {
const { dataFromApi } = this.state;
return dataFromApi ?
(<Display data={dataFromApi}>) :
(<span>loading</span>);
}
}
Hopefully that makes sense 😅.
Cheers🍻!

Relay local state management. How to add and use flag on client side?

The problem:
I want to have simple boolean flag that will be true when modal is opened and false when it is closed. And I want to update other components reactively depends on that flag
I hope there is a way to do it with relay only (Apollo has a solution for that). I don't want to connect redux of mobx or something like that (It is just simple boolean flag!).
What I already have:
It is possible to use commitLocalUpdate in order to modify your state.
Indeed I was able to create and modify my new flag like that:
class ModalComponent extends PureComponent {
componentDidMount() {
// Here I either create or update value if it exists
commitLocalUpdate(environment, (store) => {
if (!store.get('isModalOpened')) {
store.create('isModalOpened', 'Boolean').setValue(true);
} else {
store.get('isModalOpened').setValue(true);
}
});
}
componentWillUnmount() {
// Here I mark flag as false
commitLocalUpdate(environment, (store) => {
store.get('isModalOpened').setValue(false);
});
}
render() {
// This is just react component so you have full picture
return ReactDOM.createPortal(
<div
className={ styles.modalContainer }
>
dummy modal
</div>,
document.getElementById('modal'),
);
}
}
The challenge:
How to update other components reactively depends on that flag?
I can't fetch my flag like this:
const MyComponent = (props) => {
return (
<QueryRenderer
environment={ environment }
query={ graphql`
query MyComponentQuery {
isModalOpened
}`
} //PROBLEM IS HERE GraphQLParser: Unknown field `isModalOpened` on type `Query`
render={ ({ error, props: data, retry }) => {
return (
<div>
{data.isModalOpened}
<div/>
);
} }
/>);
};
Because Relay compiler throws me an error: GraphQLParser: Unknown field 'isModalOpened' on type 'Query'.
And the last problem:
How to avoid server request?
That information is stored on client side so there is no need for request.
I know there a few maybe similar questions like that and that. But they doesn't ask most difficult part of reactive update and answers are outdated.
If you need to store just one flag as you said, I recommend you to use React Context instead of Relay. You could do next:
Add Context to App component:
const ModalContext = React.createContext('modal');
export class App extends React.Component {
constructor() {
super();
this.state = {
isModalOpened: false
}
}
toggleModal = (value) => {
this.setState({
isModalOpened: value
})
};
getModalContextValue() {
return {
isModalOpened: this.state.isModalOpened,
toggleModal: this.toggleModal
}
}
render() {
return (
<ModalContext.Provider value={this.getModalContextValue()}>
//your child components
</ModalContext.Provider>
)
}
}
Get value from context everywhere you want:
const MyComponent = (props) => {
const { isModalOpened } = useContext(ModalContext);
return (
<div>
{isModalOpened}
</div>
);
};
If you will use this solution you will get rid of using additional libraries such as Relay and server requests.

React Redux - how to load details if the array is not yet obtained?

I have an app with redux and router where on the first load, all users are loaded. To this end, I've implemented a main component that loads the user when the component is mounted:
class Content extends Component {
constructor(props) {
super(props);
}
componentDidMount() {
this.props.load();
}
render() {
return this.props.children;
}
}
The afterwards, if the user chooses to load the details of one user, the details are also obtained through the same lifehook:
class Details extends Component {
componentDidMount() {
this.props.getByUrl(this.props.match.params.url);
}
render() {
const { user: userObject } = this.props;
const { user } = userObject;
if (user) {
return (
<>
<Link to="/">Go back</Link>
<h1>{user.name}</h1>
</>
);
}
return (
<>
<Link to="/">Go back</Link>
<div>Fetching...</div>
</>
);
}
Now this works well if the user lands on the main page. However, if you get directly to the link (i.e. https://43r1592l0.codesandbox.io/gal-gadot) it doesn't because the users aren't loaded yet.
I made a simple example to demonstrate my issues. https://codesandbox.io/s/43r1592l0 if you click a link, it works. If you get directly to the link (https://43r1592l0.codesandbox.io/gal-gadot) it doesn't.
How would I solve this issue?
Summary of our chat on reactiflux:
To answer your question: how would you solve this? -> High Order Components
your question comes down to "re-using the fetching all users before loading a component" part.
Let's say you want to show a Component after your users are loaded, otherwise you show the loading div: (Simple version)
import {connect} from 'react-redux'
const withUser = connect(
state => ({
users: state.users // <-- change this to get the users from the state
}),
dispatch => ({
loadUsers: () => dispatch({type: 'LOAD_USERS'}) // <-- change this to the proper dispatch
})
)
now you can re-use withUsers for both your components, which will look like:
class Content extends Component {
componentDidMount() {
if (! this.props.users || ! this.props.users.length) {
this.props.loadUsers()
}
}
// ... etc
}
const ContentWithUsers = withUsers(Content) // <-- you will use that class
class Details extends Component {
componentDidMount() {
if (! this.props.users || ! this.props.users.length) {
this.props.loadUsers()
}
}
}
const DetailsWithUsers = withUsers(Details) // <-- same thing applies
we now created a re-usable HOC from connect. you can wrap your components with withUsers and you can then re-use it but as you can see, you are also re-writing the componentDidMount() part twice
let's take the actual load if we haven't loaded it part out of your Component and put it in a wrapper
const withUsers = WrappedComponent => { // notice the WrappedComponent
class WithUsersHOC extends Component {
componentDidMount () {
if (!this.props.users || !this.props.users.length) {
this.props.loadUsers()
}
}
render () {
if (! this.props.users) { // let's show a simple loading div while we haven't loaded yet
return (<div>Loading...</div>)
}
return (<WrappedComponent {...this.props} />) // We render the actual component here
}
}
// the connect from the "simple version" re-used
return connect(
state => ({
users: state.users
}),
dispatch => ({
loadUsers: () => dispatch({ type: 'LOAD_USERS' })
})
)(WithUsersHOC)
}
Now you can just do:
class Content extends Component {
render() {
// ......
}
}
const ContentWithUsers = withUsers(Content)
No need to implement loading the users anymore, since WithUsersHOC takes care of that
You can now wrap both Content and Details with the same HOC (High Order Component)
Until the Users are loaded, it won't show the actual component yet.
Once the users are loaded, your components render correctly.
Need another page where you need to load the users before displaying? Wrap it in your HOC as well
now, one more thing to inspire a bit more re-usability
What if you don't want your withLoading component to just be able to handle the users?
const withLoading = compareFunction = Component =>
class extends React.Component {
render() {
if (! compareFunction(this.props)) {
return <Component {...this.props} />;
}
else return <div>Loading...</div>;
}
};
now you can re-use it:
const withUsersLoading = withLoading(props => !props.users || ! props.users.length)
const ContentWithUsersAndLoading = withUsers(withUsersLoading(Content)) // sorry for the long name
or, written as a bit more clean compose:
export default compose(
withUsers,
withLoading(props => !props.users || !props.users.length)
)(Content)
now you have both withUsers and withLoading reusable throughout your app

React rendering content before props is properly mapped / set

I've created a component with React using Redux where it takes two renders before the state is mapped to the props.
with this code,
'user.private' on first render is null and on the second render, it's false
because of that, loading the page flickers between showing 'login' for a second before showing hidden content
I'd like to show the login text by default, but I don't actually want it to display if the user's private field is set to false.
class Content extends React.Component {
render() {
const { user } = this.props;
let show = false;
if (user.private === false) show = true
return (
<section>
{
show
? <p>hidden content</p>
: <p>login</p>
}
</section>
)
}
}
const mapStateToProps = state => ({
user: state.store.user
});
export default connect(mapStateToProps, {})(Content)
You can use switch to handle the null case (you can either show a loader or just render nothing by returning null)
I am using the component state to illustrate the idea, but you can apply it to your redux connected Component
class App extends React.Component {
state = {
user : {
private : null
}
}
componentDidMount () {
setTimeout(() => {
this.setState(() => {
return {
user : {
private : true
}
}
});
}, 2000);
}
renderContent () {
const { user } = this.state;
switch (user.private) {
case null : return <span>Loading...</span>
case false : return <p>login</p>
default : return <p>hidden content</p>
}
}
render () {
return (
<div>
{this.renderContent()}
</div>
);
}
}
ReactDOM.render(
<App/>,
document.querySelector('#root')
);
demo
Assumming user is initially undefined or null, you can check if user and/or its property private is defined before showing anything:
if (this.props.user == null || this.props.user.private) {
show = false;
}
The double equals null will make the condition true if the value of this.props.user is undefined or null. You could also have used !this.props.user.
In case you made your initial value for user be {} even before getting it, then you would have to do:
if (this.props.user.private == null || this.props.user.private) {
show = false;
}

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.

Resources