React Relay createFragmentContainer and QueryRenderer data flow - react-relay

I've been working through the examples in relay's document for QueryRenderer https://relay.dev/docs/en/query-renderer and FragmentContainer https://relay.dev/docs/en/fragment-container
I'm confused as to how the data is meant to be accessed by the Components wrapped in the HOC createFragmentContainer.
I have a top level QueryRenderer:
function renderFunction({error, props}) {
...
if (props) {
// Added todoList as a prop with a distinct name from tdlist which
// is meant to be passed in by createFragmentContainer
return <TodoList todoList = {props.todoList} />
}
...
}
export default function ContainerWithQueryRenderer() {
return (
<QueryRenderer
environment={environment}
query={graphql`
query ContainerWithQueryRenderer_Query {
todoList {
title
todoListItems { text isComplete}
}
}`}
render = { renderFunction }
/>
);
}
and a TodoList component that defines what data is needed in a graphql fragment:
import React from 'react';
import TodoItem from './TodoItem.js';
import { createFragmentContainer } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
function TodoList(props) {
console.log('TodoList props:',props);
if (props && props.todoList && props.todoList.todoListItems && props.todoList.title) {
return (
<div className='list-container'>
<div className='list-header'>
<div className='list-header-label'>{props.todoList.title}</div>
</div>
<div className='list'>{props.todoList.todoListItems.map( (item, key) => <TodoItem key = {key} item={item} />)}</div>
</div>
);
} else {
return null;
}
}
export default createFragmentContainer(TodoList,{
tdlist: graphql`
fragment TodoList_tdlist on TodoList {
title
todoListItems {
...TodoItem_item
}
}
`,
})
and a child TodoListItem
import React from 'react';
import { createFragmentContainer } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
function TodoItem(props) {
return <div className='list-item'>
<div className='list-item-label'>{props.item.text}</div>
{props.item.isComplete ?<div className='list-item-buttons'>Done</div>:null}
</div>
}
export default createFragmentContainer( TodoItem,{
item: graphql`
fragment TodoItem_item on Todo {
text isComplete
}
`
});
My understanding is that the createFragmentContainer for the TodoList will inject the data from the TodoList_tdlist fragemnt as the TodoList props.tdlist), shaped as per the shape of the graphql query.
However, this appears to not be happening. I get a warning in the console:
Warning: createFragmentSpecResolver: Expected prop `tdlist` to be supplied to `Relay(TodoList)`, but got `undefined`. Pass an explicit `null` if this is intentional.
What is the job of the createFragmentContainer if it is not to pass in the tdlist?
I tried to pass the todoList data explicitly in by changing
return
to
return
(the same prop name passed in by createFragmentContainer, I (understandably) get a different error:
Warning: RelayModernSelector: Expected object to contain data for fragment `TodoList_tdlist`, got `{"title":"Your to-do list","todoListItems":[{"text":"Brush teeth","isComplete":false},....
I think the root of my confusion is not understanding how the fragments that define the data dependencies interact with the QueryRenderer. Do I need to define the query to pull in every possible piece of data that could ever be needed, and the point is that relay will only query what is needed by looking at the graphql fragments of the components that are being rendered now, and will re-query if that changes, updating props as it gets new data?
Do I need to pass props down to fragment containers as props explicitly, or if they are a descendant of a QueryRenderer that requests their data, will the createFragmentContainer be able to access it via the relay environment?
Here is my graphql.schema to assist:
type Query {
todoList: TodoList!
}
type Todo {
text: String!
isComplete: Boolean!
}
type TodoList {
title: String!
todoListItems: [Todo]!
}

You must name the descendant fragment container's GraphQL fragment in your QueryRenderer. Without identifying the fragment, react-relay has relay no link between the QueryRenderer and the descendant FragmentContainer components. In your question, the query prop passed to QueryRenderer is
query ContainerWithQueryRenderer_Query { todoList { title todoListItems { text isComplete }}}
instead of
query ContainerWithQueryRenderer_Query { TodoList_tdlist }
Also, because the QueryRenderer works hand in hand with the Fragment Containers, it is simpler to iterate the todoListItems in the Query Renderer, and use a fragment for the TodoItem_item. Hence below I have merged the above <ContainerWithQueryRenderer /> and <TodoList />
This approach works:
TodoListWithQueryRenderer.js:
function renderFunction({error, props}) {
...
if (props) {
return (
<div>
<div>
<div>{props.todoList.title}</div>
</div>
<div>{props.todoList.todoListItems.map( (item, key) => <TodoItem key = {key} item={item} /> )}</div>
</div>
);
}
...
}
export default function TodoListWithQueryRenderer() {
return (
<QueryRenderer
environment={environment}
query={graphql`
query TodoListWithQueryRenderer_Query { todoList {todoListItems {...TodoItem_item } title} }
`}
render = { renderFunction }
/>
);
}
with only one descendant component required as the above merges the ContainterWithQueryRenderer with TodoList components.
TodoItem.js:
import React from 'react';
import { createFragmentContainer } from 'react-relay';
import graphql from 'babel-plugin-relay/macro';
function TodoItem(props) {
return <div>
<div>{props.item.text}</div>
{props.item.isComplete ? <div>Done</div> : null}
</div>
}
export default createFragmentContainer( TodoItem,{
item: graphql`
fragment TodoItem_item on Todo {
text isComplete
}
`
});

Related

Vue3 composable with render

Is it possible to create a composable function that would use render function so it can display something?
Example:
import { h } from 'vue'
export function useErrorHandling() {
return {
render() {
return h('div', { class: 'bar', innerHTML: 'world!' })
}
}
}
<script setup>
import { useErrorHandling } from './mouse.js'
useErrorHandling()
</script>
<template>
hello
</template>
plaground with above example
Yes It is possible to do that you need just store the value returned by the composable in a variable and use it as a component
const err = useErrorHandling()
//in template
// <err />
Playground Example

Vue 3 Composition API reuse in multiple components

I have these files
App.vue, Header.vue, search.js and Search.vue
App.vue is normal and just adding different views
Header.vue has an input box
<input type="text" v-model="searchPin" #keyup="searchResults" />
<div>{{searchPin}}</div>
and script:
import useSearch from "#/compositions/search";
export default {
name: "Header",
setup() {
const { searchPin, searchResults } = useSearch();
return {
searchPin,
searchResults
};
}
};
search.js has the reusable code
import { ref } from "vue";
export default function useSearch() {
const searchPin = ref("");
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Now, this is working well.. once you add something on the input box, it is showing in the div below.
The thing I have not understood is how to use this code to a third component like Search.vue.
I have this, but its not working.
<template>
<div>
<h1 class="mt-3">Search</h1>
<div>{{ searchPin }}</div>
</div>
</template>
<script>
import useSearch from "#/compositions/search";
export default {
name: "Search",
setup() {
const { searchPin, searchResults } = useSearch();
return {
searchPin,
searchResults
};
}
};
</script>
What am I missing? Thanks.
The fix for this is very simple
instead of
import { ref } from "vue";
export default function useSearch() {
const searchPin = ref("");
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
use
import { ref } from "vue";
const searchPin = ref("");
export default function useSearch() {
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
The problem is that the searchPin is scoped to the function, so every time you call the function, it gets a new ref. This is a desirable effect in some cases, but in your case, you'll need to take it out.
Here is an example that uses both, hope it clears it up.
const {
defineComponent,
createApp,
ref
} = Vue
const searchPin = ref("");
function useSearch() {
const searchPinLoc = ref("");
function searchResults() {
return searchPin.value + "|" + searchPinLoc.value;
}
return {
searchPin,
searchPinLoc,
searchResults
};
}
const HeaderComponent = defineComponent({
template: document.getElementById("Header").innerHTML,
setup() {
return useSearch();
},
})
const SearchComponent = defineComponent({
template: document.getElementById("Search").innerHTML,
setup() {
return useSearch();
}
})
createApp({
el: '#app',
components: {
HeaderComponent, SearchComponent
},
setup() {}
}).mount('#app')
<script src="https://unpkg.com/vue#3.0.0-rc.9/dist/vue.global.js"></script>
<div id="app">
<header-component></header-component>
<search-component></search-component>
</div>
<template id="Header">
searchPin : <input type="text" v-model="searchPin" #keyup="searchResults" />
searchPinLoc : <input type="text" v-model="searchPinLoc" #keyup="searchResults" />
<div>both: {{searchResults()}}</div>
</template>
<template id="Search">
<div>
<h1 class="mt-3">Search</h1>
<div>both: {{searchResults()}}</div>
</div>
</template>
Adding flavor to #Daniel 's answer.
This is exactly what I'm struggling with regarding to best practices ATM and came to some conclusions:
Pulling the Ref outside of the composition fn would fix your problem but if you think about it, it's like sharing a single instance of a data property used in multiple places. You should be very careful with this, since ref is mutable for whoever pulls it, and will easily break unidirectional data flow.
For e.g. sharing a single Ref instance between a parent component and a child components can be compared to passing it down from parent's data to child's props, and as I assume we all know we should avoid mutating props directly
So classical answer for your question would be, move it to Vuex state and read it from there.
But if you have a small application, don't want a state manager, or simply want to take full advantage of the composition API, then my suggestion would be to at least do something of this pattern
import { ref, computed } from "vue";
const _searchPin = ref(""); // Mutable persistant prop
const searchPin = computed(() => _searchPin.value); // Readonly computed prop to expose
export default function useSearch() {
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Not more than ONE component should mutate the persistent Ref while others could only listen to the computed one.
If you find that more than one component needs access to change the ref, then that's probably a sign you should find another way to implement this (Vuex, props and events, etc...)
As I said, I am still trying to make sense of this myself and am not sure this is a good enough pattern either, but it's definitely better then simply exposing the instance.
Another option for code arrangement would be to encapsulate in 2 different access hooks
import { ref, readonly } from "vue";
const searchPin = ref(""); // Mutable persistant prop
export const useSearchSharedLogic() {
return readonly({
searchPin
})
}
const useSearchWriteLogic() {
return {
searchPin
}
}
// ----------- In another file -----------
export default function useSearch() {
const { searchPin } = useSearchSharedLogic()
function searchResults() {
return searchPin.value;
}
return {
searchPin,
searchResults
};
}
Or something of this sort (Not even sure this would work correctly as written).
Point is, don't expose a single instance directly
Another point worth mentioning is that this answer takes measure to preserve unidirectional data flow pattern. Although this is a basic proven pattern for years, it's not carved in stone. As composition patterns get clearer in the close time, IMO we might see people trying to challenge this concept and returning in some sense to bidirectional pattern like in Angular 1, which at the time caused many problems and wasn't implemented well

React children's when using HOC to wrap parent

I am using React 16.8.6 and I have the following structure:
page.js
<ParentComponent id="testData">
<ChildComponent value={data => data.text} />
</ParentComponent>
parentComponent.tsx
export default class ParentComponent extends React.PureComponent<IParentProps> {
...
render() {
const items = this.props.children;
<MiddleComponent items={items} />
}
}
ParentContainer.ts
import { withTranslation } from 'react-i18next';
import ParentComponent from './ParentComponent';
export default withTranslation()(ParentComponent);
I need to know inside of MiddleComponent the element type (not as a String but as a React element since I am going to create a new Element based on it) of each child (so, in this case I should have ChildComponent), but when I inspect with chrome, all my children have a I18nextWithTranslation type...
Any idea how to fix this? Or if this is maybe a known bug?
If I don't use any hoc at all, when I write child.type it returns me ChildComponent(props). But this is not true to when I am using hocs to wrap the parent...
The issue was very stupid...
I was importing the <ChildComponent> as a default import even though the child was not exported as default.
Basically
import ChildComponent from '' instead of import { ChildComponent } from ''
In the example below, we're setting Component.displayName on our components so we can access that property in parents. This is a super trivial example that could be expanded to work with an array of children if needed.
const ChildComponent = () => {
return <div>child render</div>
}
ChildComponent.displayName = "MyComponentName"
const ParentComponent = ({ children }) => {
// This is the type of component.. should output "MyComponentName"
const childType = children.type.displayName
return (
<div>
<h1>Render Children</h1>
{children}
</div>
)
}
function App() {
return (
<ParentComponent>
<ChildComponent />
</ParentComponent>
)
}

I am getting unrechable code warning in react native

I am following this question's first answer to create a common parent for two of my components
import React, {Component} from 'react';
import ButtonSubmit from './ButtonSubmit'
import Form from './Form'
export default class ParentofButtonandForm extends Component {
constructor() {
super();
this.state = {
username: '',
password : '',
};
}
changeFirst(receivedUN,reaceivedPW) {
this.setState({
username: receivedUN,
password:reaceivedPW
});
}
render() {
return (
<Form username={this.state.username} password={this.state.password} changeFirst={this.changeFirst.bind(this)}/>
<ButtonSubmit username={this.state.username} password={this.state.password}/>
)
}
}
But i get unrechable code error in
<ButtonSubmit username={this.state.username} password={this.state.password}/>
I dont know what i am doing wrong. I also get a ':expected' warning in this.state.username.
You are returning two components from render functions. Either you wrap <Form> and <Button> into another component, may be View OR you can return a component array from render function.
Wrapping inside View
render() {
return (
<View>
<Form .../>
<ButtonSubmit .../>
</View>
)
}
Returning array of components, link
render() {
return [
<Form .../>,
<ButtonSubmit .../>
];
}
Hope this will help!

React Router 4 can not load new content on same component with <Link>

I can't seem to trigger any other react component life cycle method other than render() when I click on a link that leads to a page that loads exactly the same component, even though the url is different. So here's my code
//index.js - the entry point
import React from 'react';
import { render } from 'react-dom';
import { BrowserRouter, Route } from 'react-router-dom';
import Config from './Settings/Config';
import App from './Components/App';
const c = new Config();
render(
<BrowserRouter basename={c.routerBaseName}>
<App />
</BrowserRouter>
, document.getElementById('root'));
Here's my App JS
// Components/App.js
import React, {Component} from 'react';
import {Route} from 'react-router-dom';
import BlogEntry from './BlogEntry';
export default class App extends Component {
render() {
console.log('app');
return (
<div>
<Route exact path="/blog/:name" component={BlogEntry} />
</div>
)
}
}
And here is my BlogEntry.js
// Components/BlogEntry.js
import React from 'react';
import { Link } from 'react-router-dom';
export default class BlogEntry extends React.Component {
async componentDidMount() {
const [r1] = await Promise.all([
fetch(`http://api.myservice.com/${this.props.match.params.name}`)
]);
this.setState({content:await r1.json()});
console.log('fetch');
}
render() {
console.log('render');
if(!this.state) return <div></div>;
if(!this.state.content) return <div></div>;
const content = this.state.content;
return (
<div id="blog-entry" className="container">
<h1>{content.title}</h1>
<div dangerouslySetInnerHTML={{__html:content.content}}></div>
<div className="related-other">
<h2>Related Content</h2>
<ul>
<li><Link to="/blog/new-york-wins-the-contest">New York Wins the Contest!</Link></li>
<li><Link to="/blog/toronto-with-some-tasty-burgers">Toronto with Some Tasty Burgers</Link></li>
</ul>
</div>
</div>
);
}
}
So what happens is that when I click on the link for Toronto with Some Tasty Burgers or New York Wins the Contest! I see the url in my web browser address bar update accordingly. But my componentDidMount does not fire. And hence no new content is fetched or loaded.
React also won't let me put an onPress event handler to the <Link> object. And even if I did, managing the history state when browser clicks back button would be a nightmare if I were to create my own onpress event handler to load pages.
So my question is, how do I make it so that clicking on one of the links actually causes the component to fetch new data and redraw and also be part of the browser back button history?
I added this to my BlogEntry.js and everything works now:
componentWillReceiveProps(nextProps) {
this.props = nextProps;
}
I don't think your proposed solution, via componentWillReceiveProps (deprecated) is good enough. It's a hack.
Why don't you keep the route id in the state (as in /blog/:id).
Then something like this:
componentDidUpdate() {
const { match: { params: { id: postId } = {} } } = this.props;
if(this.state.postId !== postId) {
// fetch content
}
}