Props in relay prepareVariables? - javascript

Hi can i access props in prepareVariables on a container ?
I have a recursive structure :
LocationList = Relay.createContainer(LocationList, {
fragments: {
location: () => Relay.QL`
fragment on LocationPart {
id, children {
id, hasChildren, value,
${LocationListItem.getFragment('location')}
}
}
`
}
});
LocationListItem = Relay.createContainer(LocationListItem, {
initialVariables: {
expanded: false
},
fragments: {
location: (variables) => Relay.QL`
fragment on LocationPart {
id, path, value, description, hasChildren,
${LocationList.getFragment('location').if(variables.expanded)}
}
`
}
});
And on the root i expand the first level by :
fragment on LocationPart {
${LocationListItem.getFragment('location', { expanded: true })}
}
I want to preserve the whole state and restore it later.
The preserve state i have covered and i pass down a object-tree with state to all nodes.
So i would like to be able to do this in prepareVariables:
prepareVariables() {
return { expanded: this.props.open[this.location.id] };
}
I tried using a contructor:
constructor(props) {
super(props);
if (props.open[props.location.id])
props.relay.setVariables({ expanded: true });
}
but then relay complains about
Expected prop location supplied to LocationList to be data fetch by Relay.
Is this possible ?

You can't - prepareVariables runs before the component is rendered, and props aren't available there. Instead, use variables passed in from the parent.

The challenge here is that the list of expanded/collapsed components is per-instance, while Relay queries are static and constructed prior to creating any components. The static query contains one LocationListItem per level, while the results will contain a list of items. This means that the query can't represent different expanded values for different list items, since the query doesn't even have multiple list items.
This is an explicit design decision in GraphQL and Relay: queries are static so that most queries can be executed in a single round trip (more explanation on the Relay docs). In complex cases such as this, you can either:
To allow fetching the data in a single pass, you can change the query itself to accept the map of expanded/collapsed items as an input argument, and adjust the return type accordingly.
If you're okay with a few extra round trips, you can continue using the same query and use setVariables in the componentDidMount lifecycle method, setting the expanded variable based on props (like you were trying to do in prepareVariables).

Related

I want to access my state variable from one component to other

I have a react query which writes the state variable- follower, and I want to access this variable in other component to find its .length can someone tell me how do I do it
const ModalFollower = ({profile}) => {
const [follower,setFollower] = useState([])
const {
data: followerName,
isLoading: followerLoading,
isFetching: followerFetching
} = useQuery(["invitations", profile?.id], () => {
getFollowers(profile?.id).then((response) => {
if (response) {
setFollower(response);
}
});
});
return(
{
!followerLoading && (
follower.map((e) => {
return(<>
<p>{e.requested_profile.Userlink}</p>
</>}
)
}
)
I want to access the length of follower in some other component
There is no need to copy data from react-query to local state, because react-query is a full-blown state manager for server state. As long as you use the same query key, you will get data from its cache. This is best abstracted away in custom hooks.
Please be aware that with the default values, you will get a "background refetch" if a new component mount, so you will see two network requests if you use it twice. That might look confusing at first, but it is intended, as it is not react-query's primary goal to reduce network requests, but to keep your data on the screen as up-to-date as possible. So when a new component mounts that uses a query, you'll get the stale data from the cache immediately, and then a background refetch will be done. This procedure is called stale-while-revalidate.
The best way to customize this behaviour is to set the staleTime property to tell react-query how long your resource is "valid". For that time, you will only get data from the cache if available. I've written about this topic in my blog here: React Query as a State Manager.
React Query also provides selectors, so if your second component is only interested in the length, this is what my code would look like:
const useInvitations = (profile, select) =>
useQuery(
["invitations", profile?.id],
() => getFollowers(profile?.id),
{
enabled: !!profile?.id
select
}
)
Note that I also added the enabled property because apparently, profile can be undefined and you likely wouldn't want to start fetching without that id.
Now we can call this in our main component:
const ModalFollower = ({profile}) => {
const { data } = useInvitations(profile)
}
and data will contain the result once the promise resolves.
In another component where we only want the length, we can do:
const { data } = useInvitations(profile, invitations => invitations.length)
and data will be of type number and you will only be subscribed to length changes. This works similar to redux selectors.

How can I fetch backend data in child component and then retrieve an object from store?

My parent component <Room/> build children components <RoomSensor/>, when parent build these children I also send to the <RoomSensor/> uuid, by this uuid I fetch sensor data from a backend.
Store is array of objects.
// Parent <Room/>
return props.sensors.map((uuid, index) => {
return <RoomSensor key={index} uuid={uuid}/>
})
// Children <RoomSensor/>
const RoomSensor = props => {
useEffect(() => {
console.log("useEffect")
props.fetchSensor(props.uuid)
}, [props.uuid])
console.log(props.sensor)
return (
<div className={"col-auto"}>
<small><b>{props.sensor.value}</b></small>
</div>
)
}
let mapStateToProps = (state, props) => {
return {
sensor: filterSensor(state, props.uuid)
}
}
let mapDispatchToProps = {
fetchSensor,
}
export default connect(mapStateToProps, mapDispatchToProps)(RoomSensor)
// Selectors
export const getSensor = (state, uuid) => {
return _.filter(state.sensors, ["uuid", uuid])[0]
}
export const filterSensor = createSelector(
getSensor,
(sensor) => sensor
)
And I cannot understand two things:
When I do refresh I get.
TypeError: Cannot read property 'uuid' of undefined
I understand that there is no data in the state yet, that's why such an error occurs. Is it possible not to render the component until the data comes from the server?
If I comment <small><b>{props.sensor.value}</b></small> no errors occur, data appears in the store, then I uncomment this line and voila everything works. But in the console I see too many component rerende. What am I doing wrong? Is there something wrong with the selector?
In general, I want each sensor component to render independently of the others.
The following is based on a few assumptions derived from the shared code and output:
Currently, there's a hard-coded list of 4 sensor UUIDs.
createSelector is from the reselect package.
_ references an import of the lodash package.
"Is it possible not to render the component until the data comes from the server?"
The short answer to this is yes. There're several approaches to achieve this, so you'll want to evaluate what fits best with the structure of the app. Here're 2 approaches for consideration:
Retrieve the list of sensors from the server. Initialize the store with an empty list and populate it upon getting data back from the server.
In getSensor, return a default value if the uuid isn't in the list.
Either way, I'd recommend adding default state to the store. This will help reduce the amount of code required to handle edge cases.
Here's a rough example of what the new reducer and selector for (1) might look like:
export const storeReducer = (state, action) => {
let nextState = state;
if (!state) {
// State is uninitialized, so give it a default value
nextState = {
sensors: [],
};
}
switch (action.type) {
case 'RECEIVE_SENSORS':
// We received sensor data, so update the value in the store
nextState = {
...nextState,
sensors: action.sensors,
};
break;
default:
break;
}
return nextState;
};
export const getSensors(state) {
return state.sensors;
}
The action upon receiving the data from the server, could look something like:
dispatch({
sensors,
type: 'RECEIVE_SENSORS',
})
"...in the console I see too many component rerende[rs]"
Without additional context, it's hard to say exactly why the re-renders are happening, but the most likely cause is that each call to props.fetchSensor(props.uuid) changes the data in the store (e.g. if it's overwriting the data).
From the console output you shared, we see that there're 16 re-renders, which would happen because:
Each of the 4 instances of RoomSensor calls fetchSensor
This results in 4 updates to the store's state
Each update to the store's state causes React to evaluate each instance of RoomSensor for re-render
Hence, 4 state updates x 4 components evaluated = 16 re-renders
React is pretty efficient and if your component returns the same value as the previous run, it knows not to update the DOM. So, the performance impact probably isn't actually that significant.
That said, if the above theory is correct and you want to reduce the number of re-renders, one way to do it would be to check whether the data you get back from the server is the same as what's already in the store and, if so, skip updating the store.
For example, fetchSensor might be updated with something like:
const existingData = getSensor(getState(), uuid);
const newData = fetch(...);
// Only update the store if there's new data or there's a change
if (!existingData || !_.isEqual(newData, existingData)) {
dispatch(...);
}
This would require updating getSensor to return a falsey value (e.g. null) if the uuid isn't found in the list of sensors.
One additional tip
In Room, RoomSensor is rendered with its key based on the item's index in the array. Since uuid should be unique, you can use that as the key instead (i.e. <RoomSensor key={uuid} uuid={uuid} />). This would allow React to base updates to RoomSensor on just the uuid instead of also considering the order of the list.

NextJS - can't identify where state change is occurring on page component when using shallow Router push

I have an app. that uses NextJS. I have a page that looks like the following:
import React from 'react'
import { parseQuery } from '../lib/searchQuery'
import Search from '../components/search'
class SearchPage extends React.Component {
static getInitialProps ({ query, ...rest }) {
console.log('GET INITIAL PROPS')
const parsedQuery = parseQuery(query)
return { parsedQuery }
}
constructor (props) {
console.log('CONSTRUCTOR OF PAGE CALLED')
super(props)
this.state = props.parsedQuery
}
render () {
return (
<div>
<div>
<h1>Search Results</h1>
</div>
<div>
<h1>DEBUG</h1>
<h2>PROPS</h2>
{JSON.stringify(this.props)}
<h2>STATE</h2>
{JSON.stringify(this.state)}
</div>
<div>
<Search query={this.state} />
</div>
</div>
)
}
}
export default SearchPage
getInitialProps is ran for SSR - it receives the query string as an object (via Express on the back end) runs it through a simple 'cleaner' function - parseQuery - which I made, and injects it into the page via props as props.parsedQuery as you can see above. This all works as expected.
The Search component is a form with numerous fields, most of which are select based with pre-defined fields and a few a number based input fields, for the sake of brevity I've omitted the mark up for the whole component. Search takes the query props and assigns them to its internal state via the constructor function.
On changing both select and input fields on the Search component this code is ran:
this.setState(
{
[label]: labelValue
},
() => {
if (!this.props.homePage) {
const redirectObj = {
pathname: `/search`,
query: queryStringWithoutEmpty({
...this.state,
page: 1
})
}
// Router.push(href, as, { shallow: true }) // from docs.
this.props.router.push(redirectObj, redirectObj, { shallow: true })
}
}
)
The intention here is that CSR takes over - hence the shallow router.push. The page URL changes but getInitialProps shouldn't fire again, and subsequent query changes are handled via componentWillUpdate etc.. I confirmed getInitialProps doesn't fire again by lack of respective console.log firing.
Problem
However, on checking/unchecking the select fields on the Search component I was surprised to find the state of SearchPage was still updating, despite no evidence of this.setState() being called.
constructor isn't being called, nor is getInitialProps, so I'm unaware what is causing state to change.
After initial SSR the debug block looks like this:
// PROPS
{
"parsedQuery": {
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": [],
"lowPrice": "",
"highPrice": ""
}
Then after checking a select field in Search surprisingly it updates to this:
// PROPS
{
"parsedQuery": {
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
}
// STATE
{
"manufacturer": ["Apple"],
"lowPrice": "",
"highPrice": ""
}
I can't find an explanation to this behaviour, nothing is output to the console and I can't find out how to track state changes origins via dev. tools.
Surely the state should only update if I were to do so via componentDidUpdate? And really shouldn't the parsedQuery prop only ever be updated by getInitialProps? As that's what created and injected it?
To add further confusion, if I change a number input field on Search (such as lowPrice), the URL updates as expected, but props nor page state changes in the debug block. Can't understand this inconsistent behaviour.
What's going on here?
EDIT
I've added a repo. which reproduces this problem on as a MWE on GitHub, you can clone it here: problem MWE repo.
Wow, interesting problem. This was a fun little puzzle to tackle.
TL;DR: This was your fault, but how you did it is really subtle. First things first, the problem is on this line:
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L17
Here in this example, it is this:
this.state = props.parsedQuery
Let's consider what is actually happening there.
In IndexPage.getInitialProps you are doing the following:`
const initialQuery = parseQuery({ ...query })
return { initialQuery }
Through Next's mechanisms, this data passes through App.getInitialProps to be returned as pageProps.initialQuery, which then becomes props.initialQuery in IndexPage, and which is then being passed wholesale through to your Search component - where your Search component then "makes a copy" of the object to avoid mutating it. All good, right?
You missed something.
In lib/searchQuery.js is this line:
searchQuery[field] = []
That same array is being passed down into Search - except you aren't copying it. You are copying props.query - which contains a reference to that array.
Then, in your Search component you do this when you change the checkbox:
const labelValue = this.state[label]
https://github.com/benlester/next-problem-example/blob/master/frontend/components/search.js#L57
You're mutating the array you "copied" in the constructor. You are mutating your state directly! THIS is why initialQuery appears to update on the home page - you mutated the manufacturers array referenced by initialQuery - it was never copied. You have the original reference that was created in getInitialProps!
One thing you should know is that even though getInitialProps is not called on shallow pushes, the App component still re-renders. It must in order to reflect the route change to consuming components. When you are mutating that array in memory, your re-render reflects the change. You are NOT mutating the initialQuery object when you add the price.
The solution to all this is simple. In your Search component constructor, you need a deep copy of the query:
this.state = { ...cloneDeep(props.query) }
Making that change, and the issue disappears and you no longer see initialQuery changing in the printout - as you would expect.
You will ALSO want to change this, which is directly accessing the array in your state:
const labelValue = this.state[label]
to this:
const labelValue = [...this.state[label]]
In order to copy the array before you change it. You obscure that problem by immediately calling setState, but you are in fact mutating your component state directly which will lead to all kinds of weird bugs (like this one).
This one arose because you had a global array being mutated inside your component state, so all those mutations were being reflected in various places.

Is react-router-relay inconsistent with the Relay pattern?

I'm using react-router-relay in a project. The design seems off to me given that every component basically ends up with a fragment having the same name as the root query. Shouldn't each component be able to have uniquely named fragments of any arbitrary type under the root query? Is this possible using this package or is my thinking flawed here?
Edit: Perhaps my question was a bit vague. My problem is that there are essentially two rules for the queries attribute defined by react-router-relay that enforce what seems to me to be a weird design pattern. Those two rules are:
Each query can only go "one level" deep.
Each query must map to a fragment with an identical name on the component that uses it.
This leaves you with a scenario whereby you either:
Use the same "viewer" query for every component and define a complimentary "viewer" fragment on each component. These fragments would all define different data requirements, despite having the same name, which seems very confusing.
You create unique fragment names for different components and then repeat the same exact root query with different names depending on the type of data you want to fetch, which seems downright silly.
Good question. When you're dealing with Relay, you're thinking is correct in that every component should have its own fragment so that the query itself maps exactly to the data needed for that particular component. The naming of the fragments can be however you like them named, but the type cannot be arbitrary. It must be a declared type underneath the Root Query object (or whatever field you are appending the fragment to). Otherwise the fragment will throw an error saying that you cannot query that type on Query or field.
For example:
var componentOneFragment = Relay.QL`
fragment on User {
name
}
`;
One thing to note here is that you don't need to have a name for fragments like fragment userFragment on User { ... }. This will give you more flexibility when referencing component fragments dynamically from Relay queries in your router by declaring ${Component.getFragment(componentOneFragment)}. Hope this helps!
EDIT:
Use the same "viewer" query for every component and define a
complimentary "viewer" fragment on each component. These fragments
would all define different data requirements, despite having the same
name, which seems very confusing.
Although the fact that the identical names of the fragments may seem confusing, this is the best way to think about things. Each component does indeed have different data requirements, so naturally their Relay containers will have different fragments, but still under the same fragment name.
This fragment may be included in one of your Relay containers that need User data:
const WidgetList = Relay.createContainer(/* ... */, {
initialVariables: {
color: null,
size: null,
limit: null
},
fragments: {
viewer: () => Relay.QL`
fragment on User {
widgets(color: $color, size: $size, first: $limit) {
edges {
node {
name,
},
},
},
}
`
}
});
While this fragment (with still the same name) may be included in another Relay container that needs Widget data:
const ActionsList = Relay.createContainer(/* ... */, {
initialVariables: {
input: null
},
fragments: {
viewer: () => Relay.QL`
fragment on Widget {
actions(input: $input) {
edges {
node {
name,
},
},
},
}
`
}
});
These can both be used dynamically (i.e. $Component.getFragment('viewer')) in the same GraphQL query as long as User and Widget are both types under the Root Query object.

Is it possible to create children nodes that provide configuration to the parent component?

The scenario where I am looking to do this is to create a generic datagrid component. I would like to use the children to define the columns of the grid, but I don't want those children to render because rendering logic is abstracted to a presentation of the grid structure (the view property below)
i.e.,
<MyDataGrid data={myData} view={myTableView}>
<MyCol fieldName='asdf' sortable />
</MyDataGrid>
Because the columns provide rendering information to the grid, they need to be accessible in the grid's render function without having first rendered themselves. From what I can tell this is not possible and I am currently just passing the column configuration as a prop on the grid. This strategy works fine but is certainly not the best looking strategy.
I know you can build instrinsic elements that can be used, but I think React still wants to manage them as DOM nodes. What i want is for react to ignore the DOM for the children of my component and let me just parse the children verbatim (i.e. as an array of MyCol { fieldName: string, sortable: boolean }).
Is this possible? scheduled on the road map? even considered?
I know it is a bit of a strange question, and I'm happy to continue with the strategy I've employed so far. But it would be nice to have the option to create these renderless "dumb" nodes.
Sure! Take a look at React Router for an example of this style of configuration. That said, I think it works better for things that are nested (like route configs); otherwise, I'd recommend just using the more JavaScript-centric style of defining an object or array of objects to configure the grid.
In your example, MyCol doesn't need to be rendered, you just want to introspect the properties it was created with. Here's an example:
var MyDataGrid = React.createClass({
render: function() {
var options = React.Children.map(this.props.children, (child) => {
return {
type: child.type.displayName,
sortable: !!child.props.sortable,
fieldName: child.props.fieldName
};
});
return <pre>{JSON.stringify(options, null, " ")}</pre>
}
});
var MyCol = React.createClass({
render: function() {
return null;
}
});
var app = (
<MyDataGrid>
<MyCol fieldName='asdf' sortable />
<MyCol fieldName='anotherColumn' />
</MyDataGrid>
);
ReactDOM.render(app, document.getElementById("app"));
Example: https://jsbin.com/totusu/edit?js,output
The main problem with processing children for configuration is that your react component class will not have it's constructor called when you receive the children props in your parent component. This makes things complicated if you have any logic associated with the configuration node/component that needs to be applied.
The pattern I am now using is as follows (and is confirmed to be working as you would expect). I am using ES6 classes for components but the pattern is applicable to the functional React.create* style as well.
Create the props interface (this defines which props can be used in your configuration child nodes)
Create a dummy component class that makes use of the props you created (this component is literally just a definition and contains no code)
Create a configuration class with a constructor that consumes all props and performs any initialization logic.
Add a static create function to your configuration class that consumes a ReactElement and returns a new instance of your configuration class (the element will have the props defined in jsx/tsx
Use React.Children.map to convert the children props into a configuration instance array in your parent (this can be done in componentWillMount and saved to an instance variable, unless your column definitions are mutable).
Simplified Example (in Typescript)
interface IDataGridColumnProps {
fieldName: string;
header?: string;
sortable?: boolean;
}
export class DataGridColumn extends React.Component<IDataGridColumnProps, any> {}
class Column {
constructor(public fieldName: string, public header?: string, public sortable = false) {
if (this.header == null) {
this.header = this.fieldName;
}
}
public static create(element: React.ReactElement<IDataGridColumnProps>) {
return new Column(element.props.fieldName, element.props.header, element.props.sortable);
}
}
export class DataGridView /* ... */ {
private columns: Column[];
componentWillMount() {
this.columns = React.Children.map(this.props.children, (x: React.ReactElement<IDataGridColumnProps>) => {
return Column.create(x);
});
}
}
The pattern used above is to essentially convert the component nodes into configuration instances on the parent component. The original children nodes are thrown out and the configuration they provide is retained in an instance array that lives within the parent component.

Categories

Resources