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

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.

Related

Vuex/Redux store pattern - sharing single source of data in parent and child components that require variations of that data

I understand the benefits of using a store pattern and having a single source of truth for data shared across components in an application, and making API calls in a store action that gets called by components rather than making separate requests in every component that requires the data.
It's my understanding that if this data needs to change in some way, depending on the component using the data, this data can be updated by calling a store action with the appropriate filters/args, and updating the global store var accordingly.
However, I am struggling to understand how to solve the issue whereby a parent component requires one version of this data, and a child of that component requires another.
Consider the following example:
In an API, there exists a GET method on an endpoint to return all people. A flag can be passed to return people who are off sick:
GET: api/people returns ['John Smith', 'Joe Bloggs', 'Jane Doe']
GET: api/people?isOffSick=true returns ['Jane Doe']
A parent component in the front end application requires the unfiltered data, but a child component requires the filtered data. For arguments sake, the API does not return the isOffSick boolean in the response, so 2 separate requests need to be made.
Consider the following example in Vue.js:
// store.js
export const store = createStore({
state: {
people: []
},
actions: {
fetchPeople(filters) {
// ...
const res = api.get('/people' + queryString);
commit('setPeople', res.data);
}
},
mutations: {
setPeople(state, people) {
state.people = people;
}
}
});
// parent.vue - requires ALL people (NO filters/args passed to API)
export default {
mounted() {
this.setPeople();
},
computed: {
...mapState([
'people'
])
},
methods: {
...mapActions(['setPeople']),
}
}
// child.vue - requires only people who are off sick (filters/args passed to API)
export default {
mounted() {
this.setPeople({ isOffSick: true });
},
computed: {
...mapState([
'people'
])
},
methods: {
...mapActions(['setPeople']),
}
}
The parent component sets the store var with the data it requires, and then the child overwrites that store var with the data it requires.
Obviously the shared store var is not compatible with both components.
What is the preferred solution to this problem for a store pattern? Storing separate state inside the child component seems to violate the single source of truth for the data, which is partly the reason for using a store pattern in the first place.
Edit:
My question is pertaining to the architecture of the store pattern, rather than asking for a solution to this specific example. I appreciate that the API response in this example does not provide enough information to filter the global store of people, i.e. using a getter, for use in the child component.
What I am asking is: where is an appropriate place to store this second set of people if I wanted to stay true to a store focused design pattern?
It seems wrong somehow to create another store variable to hold the data just for the child component, yet it also seems counter-intuitive to store the second set of data in the child component's state, as that would not be in line with a store pattern approach and keeping components "dumb".
If there were numerous places that required variations on the people data that could only be created by a separate API call, there would either be a) lots of store variables for each "variation" of the data, or b) separate API calls and state in each of these components.
Thanks to tao I've found what I'm looking for:
The best approach would be to return the isOffSick property in the API response, then filtering the single list of people (e.g. using a store getter), thus having a single source of truth for all people in the store and preventing the need for another API request.
If that was not possible, it would make sense to add a secondary store variable for isOffSick people, to be consumed by the child component.

How to get data from the second query using a variable from the first query in graphql gatsby?

I'm trying to render all photos from a directory that will be defined in the frontmatter of an mdx file called carouselPhotosDir. I thought that to do this, I would need to query the specific mdx's carouselPhotosDir frontmatter field. Then, store that into a variable and then query allFile where the relative path is equal to the carouselPhotosDir frontmatter field that I got from the first query. I'm not so sure how to do this. I tried doing the query that you see below but it just queried everything. I tested the query with a hardcoded path that is defined in the frontmatter field and it worked so the variable from the first query isnt working. Is the variable not defined? I thought it would work similarily to the id variable.
export const pageQuery = graphql`
query ProjectQuery($id: String, $carouselPhotosDir: String) {
mdx(id: { eq: $id }) {
id
body
slug
frontmatter {
title
repo
carouselPhotosDir
}
}
allFile(filter: { relativeDirectory: { eq: $carouselPhotosDir } }) {
edges {
node {
id
name
base
childImageSharp {
gatsbyImageData(
placeholder: BLURRED
layout: CONSTRAINED
transformOptions: { cropFocus: CENTER, fit: COVER }
)
}
}
}
}
}
`;
Example mdx frontmatter:
---
title: blob
author: JuanCarlos
thumbnail: ./blob-thumbnail.png
repo: https://github.com/blob/repo
carouselPhotosDir: blob/carousel-photos
order: 3
---
Short answer: you can't.
Is the variable not defined?
Indeed.
In GraphQL, fields at each "level" of the request are executed and resolved in parallel. In your example, mdx and allFile are both fields of the same type (the root query type) so they will be resolved at the same time. That means each query essentially is not aware of the other or what the other resolved to.
If you were using some GraphQL client as Apollo you can use some kind of composing to mix the queries on the client-side but not in this scenario where the queries are run in the build-time.
That said, you have two options:
When you createPage in for each template page in the gatsby-node you know which mdx is being created at that time hence you can pass the carouselPhotosDir variable as a context (see https://www.gatsbyjs.com/docs/how-to/querying-data/page-query/). In that way, carouselPhotosDir will be queried in the gatsby-node but used in the page/template query.
Use JavaScript filtering in the template once the data fetch is done. You just need to filter among allFile data the specific carouselPhotosDir for each template. You can use the same pageContext variable as before but not in a GraphQL query but in a JavaScript filtering loop to get the specific node of files.

Conditional rendering in react for a multi language app in react

I am working on a react app that is going to be launched for different countries. In each country some components will be the same but others are going to be different.
i.e
France will have
- Component A
- Component B
- Component C
Germany will have
- Component A
- Component D
So, both sites share similar components but some of them are unique.
We use a global env var to tell which site to load. (en, fr, etc)
Worth saying that the URL should be the same for all sites (the variable above should the one that tells the app which component to display)
What would be the best idea for handling this component differences?
Ideas that I`ve came up with until today:
Create one site per country. (Big problem since too much DRY. There are shared many components)
Conditional rendering (Feels hacky, since there are several countries, leading to endless if else)
High Order component that returns all the components inside a parent container component (Feels good, but I did not want to reinvent the wheel and I wanted to ask here first)
Is there a NPM package that will help me archive this?
Should I start from scratch?
Thank you very much.
not sure if I get the point (sorry if that is the case).
You don't need to create two sites. You just need to organize the site content in a passing structure (something like):
In the file with the site content:
const frenchData = [
{
idKey: 'aboutus',
textTitel: aboutUsTextTitelFR,
textField: aboutUsTextFieldFR
}
];
const englishData = [
{
idKey: 'aboutus',
textTitel: aboutUsTextTitelEN,
textField: aboutUsTextFieldEN
}
];
export const aboutusData = {
'fr': frenchData,
'en': englishData
}
You can get the preferred languages (from the user's browser) with the following lines.
static async getInitialProps({ req }) {
const userAgent = req ? req.headers['accept-language'] : navigator.userAgent;
return { userAgent };
}
After making some string formating on the userAgent result, you will get an array of preferred languages. After that you need to render the passing data (DE, EN, ES, whatever)

How to get a one-to-many field as an array in redux-orm

I have the following models for a chat application using redux-orm. Each Conversation contains many Messages, but one message can only belong to a single Conversation:
export class Message extends Model {
static modelName = 'Message';
static fields = {
id: attr(),
text: attr(),
created: attr(),
from: fk('User'),
conversation: fk('Conversation', 'messages')
};
}
export class Conversation extends Model {
static modelName = 'Conversation';
static fields = {
id: attr(),
created: attr(),
};
}
I'm using the following selector to get a list of conversations with their respective messages.
export const getConversations = createSelector(
getOrm,
createOrmSelector(orm, session => {
return session.Conversation
.all()
.toModelArray()
})
);
The problem? The messages property of each Conversation instance is a QuerySet, not an Array, which makes it difficult to deal with when passing ti components.
Here's the solutions I've tried:
Mapping the messages property of every Conversation model returned to an array of Messages with messages.all().toModelArray(). This gave me the error Can't mutate a reverse many-to-one relation, even when I tried cloning the object.
Creating an entirely new plain old JavaScript object and copying all the properties over, then setting the correct value for messages. This worked, but creating all these new objects seems like a huge performance hog on an application with frequent state changes.
How should I achieve my goal here?
You ought to be able to do something akin to:
return session.Conversation.all().toModelArray()
.map(c => ({
...c.ref,
messages: c.messages.toRefArray()
}))
in your selector. If that's not working, you might need to include more detail. You don't get relations for free with the initial toModelArray, you do need to 'query' them in order to use them in the selector.
Normally I wouldn't just dump the entire store contents for these entities like this, I'd refine 'em to what the component actually requires:
import { pick } from 'lodash/fp'
// ...
const refineConversation = c => ({
...pick([ 'id', 'updatedAt' ], c),
messages: c.messages.toRefArray().map(refineMessages)
})
const refineMessages = m => ({
...pick([ 'id', 'author', 'text', 'updatedAt' ], m)
})
// ...
return session.Conversation
.all()
.toModelArray()
.map(refineConversation)
While it can be tempting to just throw an object reference from the store at your component, there are a bunch of things on that object that your component probably doesn't need to render. If any of those things change, the component has to re-render and your selector can't use its memoised data.
Remember that when you're using object spread (or Object.assign) you're creating shallow copies. Yes, there is a cost, but anything past the first level of nesting is using a reference so you're not cloning the whole thing. Use of selectors should protect you from having to do too much work in mapStateToProps (after the first render with that data).
It's important to make good choices with state management, but the real performance hits are likely to come from other sources (unnecessary renders, waiting on API interaction, etc).

Props in relay prepareVariables?

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).

Categories

Resources