React-Apollo, don't run query on component load - javascript

I'm using the awesome https://github.com/apollographql/react-apollo library and I'm trying to see if there is a better convention to load data into components than how I'm doing it now.
I've set up my components to with the apollo HOC to load data into my components, like so:
const mainQuery = gql`
query currentProfileData($userId:String, $communityId:String!){
created: communities(id:$communityId) {
opportunities{
submittedDate
approvalDate
status
opportunity{
id
}
}
}
}
`;
const mainQueryOptions = {
options: {
variables: { userId: '_', communityId: '_' },
},
};
const ComponentWithData = compose(
graphql(mainQuery, mainQueryOptions),
)(Component);
This setup works great, but with some problems.
I end up with queries that always run twice, as I need to pass props to apollo refetch for the query. I also have to pass in some dummy data (aka the "_") to prevent useless data fetching.
I end up having to do some fancy checking in componentWillReceiveProps to prevent loading the query multiple times.
I can't use the skip option on the query as this prevents re-fetch function from being passed in.
Short of my sidestepping the HOC all together and manually running the queries through apollo directly, how can I solve this?

Just a little follow up.
With the insight of #daniel, I was able to solve my 2 primary problems, run queries with props, and skipping the query conditionally until it's ready. I just wanted to post my final code result. As you can set functions for both of these options, it helps a ton.
const mainQueryOptions = {
skip: ({ community: { id: communityId } }) => !communityId,
options: ({ community: { id: communityId }, user: { id: userId = '_' } }) => ({
variables: { userId, communityId },
}),
};
You can find more info here on the apollo api page: http://dev.apollodata.com/react/api-graphql.html#graphql

If you're utilizing your component's props as the variables used in the refetch call, there is in fact a cleaner approach. The options property can actually be a function that takes your component's props. This lets you derive your variables from the props passed to your component. It would look something like this:
const mainQueryOptions = {
options: ({ userId, communityId }) => ({
variables: { userId, communityId },
},
});

Related

Is it ok to modify Vuex state using only the payload argument of a mutation?

For example, could I iterate over Vuex data in a Vue file and choose the data needing updating, then pass the found data to an action, which commits it and then the mutation only makes the update?
The reason I'm unsure about it is because the typical format of a Vuex mutation contains the parameter for 'state', so I assume it needs to be used, and the only way to do that is either by doing all the looping inside the mutation, or to pass indexes to it to more quickly find the exact fields needing changing.
For who asked, a code example:
someVueFile.vue
computed: {
...mapState({
arrayOfObjects: (state) => state.someVuexStore.arrayOfObjects
}),
},
methods: {
myUpdateMethod() {
let toBePassedForUpdate = null;
let newFieldState = "oneValue";
this.arrayOfObjects.forEach((myObject) => {
if (myObject.someDataField !== "oneValue") {
toBePassedForUpdate = myObject.someDataField;
}
})
if (toBePassedForUpdate) {
let passObject = {
updateThis: toBePassedForUpdate,
newFieldState: newFieldState
}
this.$store.dispatch("updateMyObjectField", passObject)
}
}
}
someVuexStore.js
const state = {
arrayOfObjects: [],
/* contains some object such as:
myCoolObject: {
someDataField: "otherValue"
}
*/
}
const mutations = {
updateMyObjectField(state, data) {
data.updateThis = data.newFieldState;
}
}
const actions = {
updateMyObjectField(state, data) {
state.commit("updateMyObjectField", data);
}
}
Yes, it's alright to mutate state passed in through the payload argument rather than state. Vuex doesn't bother to distinguish between the two. In either case, it's the same state, and neither option detracts from the purposes of using mutations.
To feel more sure of that, you can ask what are the purposes of mutations and of enforcing their use. The answer is to keep a centralized, trackable location for concretely defined changes to state.
To illustrate this is a good thing, imagine an app with 1000 components, each one changing state locally, outside of a mutation, and in different ways. This could be a nightmare to debug or comprehend as a 3rd party, because you don't know how or where state changes.
So mutations enforce how and a centralized where. Neither of these are damaged by only using the payload argument in a mutation.
I would do all of the logic from one action, you can desctructured the context object in the action signature like so :
actions: {
myAction ({ state, commit, getters, dispacth } ,anyOtherParameter) {
let myVar = getters.myGetter//use a getter to get your data
//execute logic
commit('myCommit', myVar)//commit the change
}
}
If you need to do the logic in your component you can easily extract the getter and the logic from the action.

Add an async code inside a redux action generator without returning a function and using redux-thunk?

Without using redux-thunk, I want to add an object(expense) to firebase realtime database inside the action generator function before returning the action itself. I don't see the need to redux-thunk if I am able to execute the async function.
Here is the code in src/actions/expenses.js
export const addExpense = ({description= '', note= '', createdAt= 0, amount= 0} = {}) => {
const expense = {
id: uuidv4(),
description,
note,
createdAt,
amount
}
const db = firebase.database()
db.ref('expenses').push(expense)
return {
type: 'ADD',
expense
}
}
While it would technically work, this way you are making harder to write tests (see this example).
If you don't care about tests... you probably didn't need redux in the first place

Dispatch Redux action after React Apollo query returns

I'm using React Apollo to query all records in my datastore so I can create choices within a search filter.
The important database model I'm using is Report.
A Report has doorType, doorWidth, glass and manufacturer fields.
Currently when the query responds, I'm passing allReports to multiple dumb components which go through the array and just get the unique items to make a selectable list, like so..
const uniqueItems = []
items.map(i => {
const current = i[itemType]
if (typeof current === 'object') {
if (uniqueItems.filter(o => o.id !== current.id)) {
return uniqueItems.push(current)
}
} else if (!uniqueItems.includes(current)) {
return uniqueItems.push(current)
}
return
})
Obviously this code isn't pretty and it's a bit overkill.
I'd like to dispatch an action when the query returns within my SidebarFilter components. Here is the query...
const withData = graphql(REPORT_FILTER_QUERY, {
options: ({ isPublished }) => ({
variables: { isPublished }
})
})
const mapStateToProps = ({
reportFilter: { isPublished }
// filterOptions: { doorWidths }
}) => ({
isAssessment
// doorWidths
})
const mapDispatchToProps = dispatch =>
bindActionCreators(
{
resetFilter,
saveFilter,
setDoorWidths,
handleDoorWidthSelect
},
dispatch
)
export default compose(connect(mapStateToProps, mapDispatchToProps), withData)(
Filter
)
The Redux action setDoorWidths basically does the code above in the SidebarFilter component but it's kept in the store so I don't need to re-run the query should the user come back to the page.
It's very rare the data will update and the sidebar needs to change.
Hopefully there is a solution using the props argument to the graphql function. I feel like the data could be taken from ownProps and then an action could be dispatched here but the data could error or be loading, and that would break rendering.
Edit:
Query:
query ($isPublished: Boolean!){
allReports(filter:{
isPublished: $isPublished
}) {
id
oldId
dbrw
core
manufacturer {
id
name
}
doorWidth
doorType
glass
testBy
testDate
testId
isAssessment
file {
url
}
}
}
While this answer addresses the specific issue of the question, the more general question -- where to dispatch a Redux action based on the result of a query -- remains unclear. There does not, as yet, seem to be a best practice here.
It seems to me that, since Apollo already caches the query results in your store for you (or a separate store, if you didn't integrate them), it would be redundant to dispatch an action that would also just store the data in your store.
If I understood your question correctly, your intent is to filter the incoming data only once and then send the result down as a prop to the component's stateless children. You were on the right track with using the props property in the graphql HOC's config. Why not just do something like this:
const mapDataToProps = ({ data = {} }) => {
const items = data
const uniqueItems = []
// insert your logic for filtering the data here
return { uniqueItems } // or whatever you want the prop to be called
}
const withData = graphql(REPORT_FILTER_QUERY, {
options: ({ isPublished }) => ({
variables: { isPublished }
}),
props: mapDataToProps,
})
The above may need to be modified depending on what the structure of data actually looks like. data has some handy props on it that can let you check for whether the query is loading (data.loading) or has errors (data.error). The above example already guards against sending an undefined prop down to your children, but you could easily incorporate those properties into your logic if you so desired.

Can I do dispatch from getters in Vuex

Fiddle : here
I am creating a webapp with Vue 2 with Vuex. I have a store, where I want to fetch state data from a getter, What I want is if getter finds out data is not yet populated, it calls dispatch and fetches the data.
Following is my Vuex store:
const state = {
pets: []
};
const mutations = {
SET_PETS (state, response) {
state.pets = response;
}
};
const actions = {
FETCH_PETS: (state) => {
setTimeout(function() {
state.commit('SET_PETS', ['t7m12qbvb/apple_9', '6pat9znxz/1448127928_kiwi'])
}, 1000)
}
}
const getters = {
pets(state){
if(!state.pets.length){
state.dispatch("FETCH_PETS")
}
return state.pets
}
}
const store = new Vuex.Store({
state,
mutations,
actions,
getters
});
But I am getting following error:
Uncaught TypeError: state.dispatch is not a function(…)
I know I can do this, from beforeMount of Vue component, but I have multiple components which uses same Vuex store, so I have to do it in one of the components, which one should that be and how will it impact other components.
Getters can not call dispatch as they are passed the state not context of the store
Actions can call state, dispatch, commit as they are passed the context.
Getters are used to manage a 'derived state'.
If you instead set up the pets state on the components that require it then you would just call FETCH_PETS from the root of your app and remove the need for the getter
I know this is an older post and I'm not sure if this is good practice, but I did the following to dispatch from a getter in my store module:
import store from "../index"
And used the store inside my getter like this:
store.dispatch("moduleName/actionName")
I did this to make sure data was made available if it was not already present.
*edit:
I want you to be aware of this: Vue form - getters and side effects
This is related to #storsoc note.
If you need to dispatch from your getter you probably are already implementing your state wrong. Maybe a component higher up should already have fetched the data before (state lifting). Also please be aware that getters should only be used when you need to derive other data from the current state before serving it to your template otherwise you could call state directly: this.$store.state.variable to use in methods/computed properties.
Also thing about your lifecycle methods.. you could for example in your mounted or created methods check if state is set and otherwise dispatch from there. If your getter / "direct state" is inside a computed property it should be able to detect changes.
had the same Problem.. also wanted all Vue-Instances to automaticly load something, and wrote a mixin:
store.registerModule('session', {
namespaced: true,
state: {
session: {hasPermission:{}},
sessionLoaded:false
},
mutations: {
changeSession: function (state, value)
{
state.session = value;
},
changeSessionLoaded: function (state)
{
state.sessionLoaded = true;
}
},
actions: {
loadSession(context)
{
// your Ajax-request, that will set context.state.session=something
}
}
});
Vue.mixin({
computed: {
$session: function () { return this.$store.state.session.session; },
},
mounted:function()
{
if(this.$parent==undefined && !this.$store.state.session.sessionLoaded)
{
this.$store.dispatch("session/loadSession");
this.$store.commit("changeSessionLoaded");
}
},
});
because it loads only one per vue-instance and store and it it inlcuded automaticly in every vue-instance, there is no need to define it in every main-app
I use a getter to configure a dynamic page. Essentially, something like this:
getter: {
configuration: function () {
return {
fields: [
{
component: 'PlainText',
props: {},
setPropsFromPageState: function (props, pageState, store) {
// custom logic
}
}
]
};
}
}
Then in the page component, when I am dynamically setting the props on a dynamic component, I can call the setPropsFromPageState(field.props, this.details, this.$store) method for that component, allowing logic to be set at the config level to modify the value of the props being passed in, or to commit/dispatch if needed.
Basically this is just a callback function stored in the getter that is executed in the component context with access to the $store via it.

What type of Relay mutator configuration is appropriate for inserting a new record?

I'm diving into GraphQL and Relay. So far, everything has been relatively smooth and easy, for me, to comprehend. I've got a GraphQL schema going with Accounts and Teams. There is no relationships between the two, yet. I've got some Relay-specific GraphQL adjustments for connections, for both the accounts and teams. Here's an example query for those two connections ...
{
viewer {
teams {
edges {
node {
id,
name
}
}
}
accounts {
edges {
node {
id,
username
}
}
}
}
}
I've got a GraphQL mutation ready to go that creates a new account. Here's the GraphQL representation of that ...
type Mutation {
newAccount(input: NewAccountInput!): NewAccountPayload
}
input NewAccountInput {
username: String!
password: String!
clientMutationId: String!
}
type NewAccountPayload {
account: Account
clientMutationId: String!
}
type Account implements Node {
id: ID!
username: String!
date_created: String!
}
I'm now trying to create my client-side Relay mutation that uses this GraphQL mutation. I'm thoroughly confused as to how to do this correctly, though. I've followed examples and nothing I come up with even seems to run correctly. I tend to get errors relating to fragment composition.
If I were writing a Relay mutation that uses this GraphQL mutation, what would the appropriate mutator configuration be? Should I be using RANGE_ADD?
For your client side mutation, you can use something like this:
export default class AddAccountMutation extends Relay.Mutation {
static fragments = {
viewer: () => Relay.QL`
fragment on Viewer {
id,
}
`,
};
getMutation() {
return Relay.QL`mutation{addAccount}`;
}
getVariables() {
return {
newAccount: this.props.newAccount,
};
}
getFatQuery() {
return Relay.QL`
fragment on AddAccountPayload {
accountEdge,
viewer {
accounts,
},
}
`;
}
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'viewer',
parentID: this.props.viewer.id,
connectionName: 'accounts',
edgeName: 'accountEdge',
rangeBehaviors: {
'': 'append',
},
}];
}
getOptimisticResponse() {
return {
accountEdge: {
node: {
userName: this.props.newAccount.userName,
},
},
viewer: {
id: this.props.viewer.id,
},
};
}
}
Then, in your GraphQL schema, you'll need to return the newly created edge, as well as the cursor:
var GraphQLAddAccountMutation = mutationWithClientMutationId({
name: 'AddAccount',
inputFields: {
newAccount: { type: new GraphQLNonNull(NewAccountInput) }
},
outputFields: {
accountEdge: {
type: GraphQLAccountEdge,
resolve: async ({localAccountId}) => {
var account = await getAccountById(localAccountId);
var accounts = await getAccounts();
return {
cursor: cursorForObjectInConnection(accounts, account)
node: account,
};
}
},
viewer: {
type: GraphQLViewer,
resolve: () => getViewer()
},
},
mutateAndGetPayload: async ({ newAccount }) => {
var localAccountId = await createAccount(newAccount);
return {localAccountId};
}
});
var {
connectionType: AccountsConnection,
edgeType: GraphQLAccountEdge,
} = connectionDefinitions({
name: 'Account',
nodeType: Account,
});
You'll need to substitute the getAccounts(), getAccountById() and createAccount method calls to whatever your server/back-end uses.
There may be a better way to calculate the cursor without having to do multiple server trips, but keep in mind the Relay helper cursorForObjectInConnection does not do any kind of deep comparison of objects, so in case you need to find the account by an id in the list, you may need to do a custom comparison:
function getCursor(dataList, item) {
for (const i of dataList) {
if (i._id.toString() === item._id.toString()) {
let cursor = cursorForObjectInConnection(dataList, i);
return cursor;
}
}
}
Finally, add the GraphQL mutation as 'addAccount' to your schema mutation fields, which is referenced by the client side mutation.
Right now, I'm following a roughly 5 step process to define mutations:
Define the input variables based on what portion of the graph you are targeting - in your case, it's a new account, so you just need the new data
Name the mutation based on #1 - for you, that's AddAccountMutation
Define the fat query based on what is affected by the mutation - for you, it's just the accounts connection on viewer, but in the future I'm sure your schema will become more complex
Define the mutation config based on how you can intersect the fat query with your local graph
Define the mutation fragments you need to satisfy the requirements of #1, #3 and #4
Generally speaking, step #4 is the one people find the most confusing. That's because it's confusing. It's hard to summarize in a Stack Overflow answer why I feel this is good advice but... I recommend you use FIELDS_CHANGE for all your mutations*. It's relatively easy to explain and reason about - just tell Relay how to look up the nodes corresponding to the top level fields in your mutation payload. Relay will then use the mutation config to build a "tracked query" representing everything you've requested so far, and intersect that with the "fat query" representing everything that could change. In your case, you want the intersected query to be something like viewer { accounts(first: 10) { edges { nodes { ... } } }, so that means you're going to want to make sure you've requested the existing accounts somewhere already. But you almost certainly have, and if you haven't... maybe you don't actually need to make any changes locally for this mutation!
Make sense?
EDIT: For clarity, here's what I mean for the fat query & configs.
getConfigs() {
return [
{
type: "FIELDS_CHANGE",
fieldIDs: {
viewer: this.props.viewer.id
}
}]
}
getFatQuery() {
return Relay.QL`
fragment on AddAccountMutation {
viewer {
accounts
}
}
`
}
*addendum: I currently believe there are only one or two reasons not to use FIELDS_CHANGE. The first is that you can't reliably say what fields are changing, so you want to just manipulate the graph directly. The second is because you decide you need the query performance optimizations afforded by the more specific variants of FIELDS_CHANGE like NODE_DELETE, RANGE_ADD, etc.

Categories

Resources