How To Split Mobx State Tree Models Across Multiple Files? - mobx-state-tree

I have a Mobx State Tree model that has grown too long and I would like to split it across multiple javascript files.
Here is a demo of some of the code:
///file1.js
import { types } from "mobx-state-tree";
export const ExampleModel = types
.model("Example", {
id: types.identifier,
name: types.optional(types.string, ""),
anotherName: types.optional(types.string, ""),
})
.views(self => ({
get test() {
return "test"
}
}))
.views(self => ({
get anotherTest() {
return "anotherTest"
}
}))
.actions(self => ({
setName(name) {
self.name = name
}
}))
.actions(self => ({
setAnotherName(name) {
self.anotherName = name
}
}))
What I want is to split this between two files, like:
///file1.js
import { types } from "mobx-state-tree";
export const ExampleModel = types
.model("Example", {
id: types.identifier,
name: types.optional(types.string, ""),
anotherName: types.optional(types.string, ""),
})
.views(self => ({
get test() {
return "test"
}
}))
.actions(self => ({
setName(name) {
self.name = name
}
}))
///file2.js
import { ExampleModel } from "./file1.js";
ExampleModel.views(self => ({
get anotherTest() {
return "anotherTest"
}
})).actions(self => ({
setAnotherName(name) {
self.anotherName = name
}
}))
You can see here that I am attempting to move a view and and action to a separate javascript file. I expect I need to do some kind of import and export between these two files, but I can't figure out how to do it.
I know that Mobx State Tree has compose functionality, as shown here:
https://nathanbirrell.me/notes/composition-mobx-state-tree/
But I am afer something more simple than this... I don't want to set up multiple models, I just need the ability to spread a model across multiple javascript files.

We do that all the time.
Just export your actions and views separately:
// file1.js
import { types } from "mobx-state-tree"
export const props = {
id: types.identifier,
name: types.optional(types.string, ""),
anotherName: types.optional(types.string, ""),
}
export const views = self => ({
get test() {
return "test"
}
})
export const actions = self => ({
setName(name) {
self.name = name
}
})
Then, create the final store from them:
// store.js
import { types } from "mobx-state-tree"
import * as file1 from "./file1"
import * as file2 from "./file2"
const Store = types
.model('Store')
.props(file1.props)
.views(file1.views)
.actions(file1.actions)
.props(file2.props)
.views(file2.views)
.actions(file2.actions)
export default Store
You can also create your own stores for testing, only from one file:
// __tests__/file1.js
import { types } from "mobx-state-tree"
import { actions, views, props } from "./file1"
const Store = types
.model('Store')
.props(props)
.views(views)
.actions(actions)
const store = Store.create(myTestSnapshot)
test('setName should set the name prop', () => {
store.setName('john')
expect(store.name).toBe('john')
})

Expressive, flexible and easy model composition is one of the best features in mobx-state-tree! :)
Here are two examples, taken straight from the relevant section in the docs:
const Square = types
.model("Square", {
width: types.number
})
.views(self => ({
surface() {
return self.width * self.width
}
}))
// create a new type, based on Square
const Box = Square
.named("Box")
.views(self => {
// save the base implementation of surface
const superSurface = self.surface
return {
// super contrived override example!
surface() {
return superSurface() * 1
},
volume() {
return self.surface * self.width
}
}
}))
// no inheritance, but, union types and code reuse
const Shape = types.union(Box, Square)
And another one:
const CreationLogger = types.model().actions(self => ({
afterCreate() {
console.log("Instantiated " + getType(self).name)
}
}))
const BaseSquare = types
.model({
width: types.number
})
.views(self => ({
surface() {
return self.width * self.width
}
}))
export const LoggingSquare = types
.compose(
// combine a simple square model...
BaseSquare,
// ... with the logger type
CreationLogger
)
// ..and give it a nice name
.named("LoggingSquare")
Applying that to your needs: Square and Box can be in different files, where Box.js imports Square from Square.js in the first example.
Same exact technique can be applied to the second example.

Related

How to get random records from Strapi content API

I have records in strapi. I am using strapi content API. In my front-end, I need to display only 2 records randomly. For limiting, I have used limit query from content API. But random fetching what keyword I need to use. The official documentation doesn't provide any details regarding this - https://strapi.io/documentation/v3.x/content-api/parameters.html#available-operators
There's no official Strapi API parameter for random. You have to implement your own. Below is what I've done previously, using Strapi v3:
1 - Make a service function
File: api/mymodel/services/mymodel.js
This will contain our actual random query (SQL), and wrapping it in a service is handy because it can be used in many places (cron jobs, inside other models, etc).
module.exports = {
serviceGetRandom() {
return new Promise( (resolve, reject) => {
// There's a few ways to query data.
// This example uses Knex.
const knex = strapi.connections.default
let query = knex('mydatatable')
// Add more .select()'s if you want other fields
query.select('id')
// These rules enable us to get one random post
query.orderByRaw('RAND()')
query.limit(1)
// Initiate the query and do stuff
query
.then(record => {
console.log("getRandom() record: %O", record[0])
resolve(record[0])
})
.catch(error => {
reject(error)
})
})
}
}
2 - Use the service somewhere, like a controller:
File: api/mymodel/controllers/mymodel.js
module.exports = {
//(untested)
getRandom: async (ctx) => {
await strapi.services.mymodel.serviceGetRandom()
.then(output => {
console.log("getRandom output is %O", output.id)
ctx.send({
randomPost: output
}, 200)
})
.catch( () => {
ctx.send({
message: 'Oops! Some error message'
}, 204) // Place a proper error code here
})
}
}
3 - Create a route that points to this controller
File: api/mymodel/config/routes.json
...
{
"method": "GET",
"path": "/mymodelrandom",
"handler": "mymodel.getRandom",
"config": {
"policies": []
}
},
...
4 - In your front-end, access the route
(However you access your API)
e.g. ajax call to /api/mymodelrandom
There is no API parameter for getting a random result.
So: FrontEnd is the recommended solution for your question.
You need to create a random request range and then get some random item from this range.
function getRandomInt(max) {
return Math.floor(Math.random() * Math.floor(max));
}
const firstID = getRandomInt(restaurants.length);
const secondID = getRandomInt(3);
const query = qs.stringify({
id_in:[firstID,secondID ]
});
// request query should be something like GET /restaurants?id_in=3&id_in=6
One way you can do this reliably is by two steps:
Get the total number of records
Fetch the number of records using _start and _limit parameters
// Untested code but you get the idea
// Returns a random number between min (inclusive) and max (exclusive)
function getRandomArbitrary(min, max) {
return Math.random() * (max - min) + min;
}
const { data: totalNumberPosts } = await axios.get('/posts/count');
// Fetch 20 posts
const _limit = 20;
// We need to be sure that we are not fetching less than 20 posts
// e.g. we only have 40 posts. We generate a random number that is 30.
// then we would start on 30 and would only fetch 10 posts (because we only have 40)
const _start = getRandomArbitrary(0, totalNumberPosts - _limit);
const { data: randomPosts } = await axios.get('/posts', { params: { _limit, _start } })
The problem with this approach is that it requires two network requests but for my needs, this is not a problem.
This seem to work for me with Strapi v.4 REST API
Controller, Get 6 random entries
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries].sort(() => 0.5 - Math.random());
ctx.body = randomEntries.slice(0, numberOfEntries);
},
};
});
Route
random.js
"use strict";
module.exports = {
routes: [
{
method: "GET",
path: "/artwork/random",
handler: "artwork.random",
config: {
auth: false,
},
},
],
};
API
http://localhost:1337/api/artwork/random
To match default data structure of Strapi
"use strict";
/**
* artwork controller
*/
const { createCoreController } = require("#strapi/strapi").factories;
module.exports = createCoreController("api::artwork.artwork", ({ strapi }) => {
const numberOfEntries = 6;
return {
async random(ctx) {
const entries = await strapi.entityService.findMany(
"api::artwork.artwork",
{
populate: ["image", "pageHeading", "seo", "socialMedia", "artist"],
}
);
const randomEntries = [...entries]
.sort(() => 0.5 - Math.random())
.slice(0, numberOfEntries);
const structureRandomEntries = {
data: randomEntries.map((entry) => {
return {
id: entry.id,
attributes: entry,
};
}),
};
ctx.body = structureRandomEntries;
},
};
});
There is also a random sort plugin.
https://www.npmjs.com/package/strapi-plugin-random-sort
This seem to work for me with Strapi v4.3.8 and graphql
src/index.js
"use strict";
module.exports = {
register({ strapi }) {
const extensionService = strapi.service("plugin::graphql.extension");
const extension = ({ strapi }) => ({
typeDefs: `
type Query {
randomTestimonial: Testimonial
}
`,
resolvers: {
Query: {
randomTestimonial: async (parent, args) => {
const entries = await strapi.entityService.findMany(
"api::testimonial.testimonial"
);
const sanitizedRandomEntry =
entries[Math.floor(Math.random() * entries.length)];
return sanitizedRandomEntry;
},
},
},
resolversConfig: {
"Query.randomTestimonial": {
auth: false,
},
},
});
extensionService.use(extension);
},
bootstrap({ strapi }) {},
};
graphql query:
query GetRandomTestimonial {
randomTestimonial {
__typename
name
position
location
description
}
}
generate random testimonial on route change/refresh
https://jungspooner.com/biography

Mock api in react redux-thunk project returning undefined

I am quite new in redux world and have not yet had a project structured the ducks way. I am trying to understand it and use it to make a mock api, since I don't have the backend ready yet. I am working with the legacy code, that I am trying to figure out. There is a folder called data, that has a duck and a backendApi file. Duck file looks like this.
data/duck.jsx
import { createSelector } from 'reselect';
import { createReduxApi } from './backendApi';
const getDataContext = state => state.default.dataContext;
const backendReduxApi = createBackendReduxApi(getDataContext);
// Action creators
export const makeRestApiRequest = endpointName => backendReduxApi .makeRequestActionCreator(endpointName);
export const resetRestApi = endpointName => backendReduxApi .makeResetActionCreator(endpointName);
// Reducers
export const dataReducer = backendReduxApi .createReducer();
// Selectors
const getRestApiState = endpointName => backendReduxApi .getEndpointState(endpointName);
export const getRestApiData = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.data);
export const getRestApiMeta = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.meta);
export const getRestApiError = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.error);
export const getRestApiStarted = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.started);
export const getRestApiFinished = endpointName => createSelector([getRestApiState(endpointName)], apiState => apiState.finished);
The backendApi.jsx file looks like this:
data/backendApi.jsx
import ReduxRestApi from './rest/ReduxRestApi';
export const BackendApi = { // NOSONAR
LANGUAGE_FILE: 'languageFile',
EMPLOYEE: 'employee',
};
const backendReduxApiBuilder = ReduxRestApi.build()
/* /api */
/* /api/employee */
.withGet('/myproject/api/employee', BackendApi.EMPLOYEE)
/* /language*/
.withGet('/myproject/language/nb_NO.json', BackendApi.LANGUAGE_FILE)
export const createBackendReduxApi = restApiSelector => backendReduxApiBuilder
.withRestApiSelector(restApiSelector)
.create();
Then in the data/rest folder I have 4 files: ReduxRestApi, restConfig, RestDuck and restMethods.
data/rest/ReduxRestApi.jsx
import { combineReducers } from 'redux';
import { get, post, postAndOpenBlob } from './restMethods';
import RestDuck from './RestDuck';
class ReduxRestApi {
constructor(endpoints, getRestApiState) {
this.createReducer = this.createReducer.bind(this);
this.getEndpoint = this.getEndpoint.bind(this);
this.makeRequestActionCreator = this.makeRequestActionCreator.bind(this);
this.makeResetActionCreator = this.makeResetActionCreator.bind(this);
this.getEndpointState = this.getEndpointState.bind(this);
this.ducks = endpoints.map(({ name, path, restMethod }) => new RestDuck(name, path, restMethod, getRestApiState));
}
createReducer() {
const reducers = this.ducks
.map(duck => ({ [duck.name]: duck.reducer }))
.reduce((a, b) => ({ ...a, ...b }), {});
return combineReducers(reducers);
}
getEndpoint(endpointName) {
return this.ducks.find(duck => duck.name === endpointName)
|| { actionCreators: {} };
}
makeRequestActionCreator(endpointName) {
return this.getEndpoint(endpointName).actionCreators.execRequest;
}
makeResetActionCreator(endpointName) {
return this.getEndpoint(endpointName).actionCreators.reset;
}
getEndpointState(endpointName) {
return this.getEndpoint(endpointName).stateSelector;
}
static build() {
class RestApiBuilder {
constructor() {
this.withGet = this.withGet.bind(this);
this.withPost = this.withPost.bind(this);
this.withPostAndOpenBlob = this.withPostAndOpenBlob.bind(this);
this.withRestApiSelector = this.withRestApiSelector.bind(this);
this.endpoints = [];
}
withGet(path, name) {
this.endpoints.push({ path, name, restMethod: get });
return this;
}
withPost(path, name) {
this.endpoints.push({ path, name, restMethod: post });
return this;
}
withPostAndOpenBlob(path, name) {
this.endpoints.push({ path, name, restMethod: postAndOpenBlob });
return this;
}
withRestApiSelector(restApiSelector) {
this.restApiSelector = restApiSelector;
return this;
}
create() {
return new ReduxRestApi(
this.endpoints,
this.restApiSelector
);
}
}
return new RestApiBuilder();
}
}
export default ReduxRestApi;
restConfig.jsx
import axios from 'axios';
import { removeErrorMessage, showErrorMessage } from '../../app/duck';
import { is401Error, isHandledError } from '../../app/ErrorTypes';
const isDevelopment = process.env.NODE_ENV === 'development';
const configureRequestInterceptors = (store) => {
const onRequestAccepted = (config) => {
store.dispatch(removeErrorMessage());
return config;
};
const onRequestRejected = error => Promise.reject(error);
axios.interceptors.request.use(onRequestAccepted, onRequestRejected);
};
const configureResponseInterceptors = (store) => {
const onSuccessResponse = response => response;
const onErrorResponse = (error) => {
if (is401Error(error) && !isDevelopment) {
window.location.reload();
}
if (!isHandledError(error)) {
store.dispatch(showErrorMessage(error));
}
return Promise.reject(error);
};
axios.interceptors.response.use(onSuccessResponse, onErrorResponse);
};
const configureRestInterceptors = (store) => {
configureRequestInterceptors(store);
configureResponseInterceptors(store);
};
export default configureRestInterceptors;
data/rest/RestDuck.jsx
import { createSelector } from 'reselect';
import { get, getBlob, post, postAndOpenBlob, postBlob } from './restMethods';
/**
* getMethodName
* Helper function that maps given AJAX-method to a name
*
* Ex. getMethodName(getBlob) -> 'GET'
*/
const getMethodName = (restMethod) => {
switch (restMethod) {
case get:
case getBlob:
return 'GET';
case post:
case postBlob:
case postAndOpenBlob:
return 'POST';
default:
return '';
}
};
/**
* createRequestActionType
* Helper function to generate actionType for actions related to AJAX calls
*
* Ex: createRequestActionType('fetchEmployee', 'ERROR', get, '/myproject/api/employee') -> '##REST/fetchEmployee GET /myproject/api/employeeERROR'
*/
const createRequestActionType = (name, qualifier, restMethod = '', path = '') => [`##REST/${name}`, getMethodName(restMethod), path, qualifier]
.filter(s => s !== '')
.join(' ');
/**
* createRequestActionTypes
* Helper function to generate ActionTypes for a given AJAX method and resource.
*
* Ex. createRequestActionType(fetchEmployee, get, '/myproject/api/employee') -> {
* reset: '##REST GET /myproject/api/employee RESET',
* requestStarted: '##REST GET /myproject/api/employee STARTED',
* requestError: '##REST GET /myproject/api/employee ERROR',
* requestFinished: '##REST GET /myproject/api/employee FINISHED',
* }
*/
const createRequestActionTypes = (name, restMethod, path) => ({
reset: createRequestActionType(name, 'RESET'),
requestStarted: createRequestActionType(name, 'STARTED', restMethod, path),
requestError: createRequestActionType(name, 'ERROR', restMethod, path),
requestFinished: createRequestActionType(name, 'FINISHED', restMethod, path)
});
/**
* createRequestThunk
* Helper function that generates a thunk that performs an AJAX call specified by 'restMethod' and 'restEndpoint'
*
* When the thunk is running, the action 'requestStarted' will be dispatched immediately.
* Then, it performs the AJAX call that returns a promise.
* If the call goes well, the action 'requestFinished' will be dispatched with data from the call.
* If the call fails, the action 'requestError' is dispatched with the contents of the error.
*/
const createRequestThunk = (restMethod, restEndpoint, requestStarted, requestFinished, requestError) => (
(params, options = {}) => (dispatch) => {
dispatch(requestStarted(params, options));
return restMethod(restEndpoint, params)
.catch((error) => {
const data = error.response && error.response.data ? error.response.data : error;
dispatch(requestError(data));
return Promise.reject(error);
})
.then((response) => {
dispatch(requestFinished(response.data));
return response;
});
}
);
/**
* createRequestActionCreators
* Helper function that creates action creators 'requestStarted', 'requestFinished' and 'requestError',
* #see createRequestThunkCreator
*/
const createRequestActionCreators = (restMethod, restEndpoint, actionTypes) => {
const reset = () => ({ type: actionTypes.reset });
const requestStarted = (params, options = {}) => ({ type: actionTypes.requestStarted, payload: { params, timestamp: Date.now() }, meta: { options } });
const requestFinished = data => ({ type: actionTypes.requestFinished, payload: data });
const requestError = error => ({ type: actionTypes.requestError, payload: error });
const execRequest = createRequestThunk(restMethod, restEndpoint, requestStarted, requestFinished, requestError);
return {
reset, requestStarted, requestFinished, requestError, execRequest
};
};
/**
* createRequestReducer
*
* Helper function that creates a reducer for an AJAX call.
* Reducer alters the state of the actions with the name defined by
* actionTypes.requestStarted
* actionTypes.requestFinished
* actionTypes.requestError
*/
const createRequestReducer = (restMethod, resourceName, actionTypes) => {
const initialState = {
data: undefined,
meta: undefined,
error: undefined,
started: false,
finished: false
};
return (state = initialState, action = {}) => {
switch (action.type) {
case actionTypes.requestStarted:
return {
...initialState,
data: action.meta.options.keepData ? state.data : initialState.data,
started: true,
meta: action.payload
};
case actionTypes.requestFinished:
return {
...state,
started: false,
finished: true,
data: action.payload
};
case actionTypes.requestError:
return {
...state,
started: false,
error: action.payload
};
case actionTypes.reset:
return {
...initialState
};
default:
return state;
}
};
};
/**
* RestDuck
* Class that offers action types, action creators, reducers and selectors for an AJAX call.
* #see createRequestActionTypes
* #see createRequestActionCreators
* #see createRequestReducer
*
* Ex.
* const getEmployeeDuck = new RestDuck(execGetRequest, 'employee', GET_EMPLOYEE_SERVER_URL);
* // Action creators
* export const fetchEmployee = getEmployeeDuck.actionCreators.execRequest;
* // Reducer
* export const dataReducer = combineReducers(
* ...,
* getEmployeeDuck.reducer,
* }
* // Selectors
* export const getDataContext = state => state.default.dataContext;
* export const getEmployeeData = getEmployeeDuck.selectors.getRequestData(getDataContext);
* export const getEmployeeStarted = getEmployeeDuck.selectors.getRequestStarted(getDataContext);
* ...
*/
class RestDuck {
constructor(name, path, restMethod, getApiContext) {
this.restMethod = restMethod;
this.name = name;
this.path = path;
this.getApiContext = getApiContext;
this.$$duck = {}; // for class internal use
}
get actionTypes() {
if (!this.$$duck.actionTypes) {
this.$$duck.actionTypes = createRequestActionTypes(this.name, this.restMethod, this.path);
}
return this.$$duck.actionTypes;
}
get actionCreators() {
if (!this.$$duck.actionCreators) {
this.$$duck.actionCreators = createRequestActionCreators(this.restMethod, this.path, this.actionTypes);
}
return this.$$duck.actionCreators;
}
get reducer() {
if (!this.$$duck.reducer) {
this.$$duck.reducer = createRequestReducer(this.restMethod, this.name, this.actionTypes);
}
return this.$$duck.reducer;
}
get stateSelector() {
return createSelector([this.getApiContext], restApiContext => restApiContext[this.name]);
}
}
export default RestDuck;
data/rest/restMethods.jsx
import axios, { CancelToken } from 'axios';
const openPreview = (data) => {
if (window.navigator.msSaveOrOpenBlob) {
window.navigator.msSaveOrOpenBlob(data);
} else {
window.open(URL.createObjectURL(data));
}
};
const cancellable = (config) => {
let cancel;
const request = axios({
...config,
cancelToken: new CancelToken((c) => { cancel = c; })
});
request.cancel = cancel;
return request.catch(error => (axios.isCancel(error) ? Promise.reject(new Error(null)) : Promise.reject(error)));
};
const defaultHeaders = {
'Cache-Control': 'no-cache',
Pragma: 'no-cache',
Expires: 0
};
const defaultPostHeaders = {
'Content-Type': 'application/json'
};
export const get = (url, params, responseType = 'json') => cancellable({
url,
params,
responseType,
method: 'get',
headers: {
...defaultHeaders
}
});
export const post = (url, data, responseType = 'json') => cancellable({
url,
responseType,
data: JSON.stringify(data),
method: 'post',
headers: {
...defaultHeaders,
...defaultPostHeaders
},
cache: false
});
export const getBlob = (url, params) => get(url, params, 'blob');
export const postBlob = (url, data) => post(url, data, 'blob');
export const postAndOpenBlob = (url, data) => postBlob(url, data)
.then((response) => {
openPreview(response.data);
return {
...response,
data: 'blob opened as preview' // Don't waste memory by storing blob in state
};
});
I am not sure where to place and how to do mock api calls in this structure. I was thinking of making a mock api similiar to this one, where I would mimick the ajax calls and store them in the redux, but just not sure how to do this in this kind of setup?
I have tried with making the mockApi folder and instead of using the restMethods, to use the file where I would write promises that would resolve the mockData. This is my attempt:
mockRestMethods
const employee = {
name: 'Joe Doe'
}
const data = {
employee
};
export const get = item => new Promise((resolve) => {
setTimeout(() => {
resolve({ data: data[item] });
}, 1000);
});
But, if I inspect what is returned as the response.data inside the createRequestThunk function in the RestDuck file I get data: undefined there. Why is that, what am I doing wrong?
I may well have this wrong, but it seems like you are replacing
export const get = (url, params, responseType = 'json') => cancellable({
with export const get = item => new Promise((resolve) => { which has a different API.
Regardless, have you tried logging the value of item in the mock get function. I'm guessing it isn't "employee" which is the only property in data.
Yes, that was my goal, to replace the call that was pointing to the backend API, with the call where I would return the mock data. I have tried to log the value of the item, but I get undefined
ok, so there's an awful lot of abstraction going on there. Id suggest starting by replacing get in data/rest/restMethods.jsx directly with a version that returns a promise, get it working, and then break it out. That way you're not dealing with too many unknowns at once.
I had done similar using redux-saga. After debugging, I had found that there must be data property as root key. Here's how you should do:
const employee = {
data: { // root key of employee
items: [
{ name: 'Bhojendra' },
{ name: 'Rauniyar' }
]
}
}
// and no need to use setTimeout, we're just resolving some constant data
export const getItems = item => new Promise(resolve => resolve(employee))
Now, I hope you know why data is undefined with your code.
Still not clear?
Response looks for the data property. That's it.

How to chain two GraphQL queries in sequence using Apollo Client

I am using Apollo Client for the frontend and Graphcool for the backend. There are two queries firstQuery and secondQuery that I want them to be called in sequence when the page opens. Here is the sample code (the definition of TestPage component is not listed here):
export default compose(
graphql(firstQuery, {
name: 'firstQuery'
}),
graphql(secondQuery, {
name: 'secondQuery' ,
options: (ownProps) => ({
variables: {
var1: *getValueFromFirstQuery*
}
})
})
)(withRouter(TestPage))
I need to get var1 in secondQuery from the result of firstQuery. How can I do that with Apollo Client and compose? Or is there any other way to do it? Thanks in advance.
The props added by your firstQuery component will be available to the component below (inside) it, so you can do something like:
export default compose(
graphql(firstQuery, {
name: 'firstQuery'
}),
graphql(secondQuery, {
name: 'secondQuery',
skip: ({ firstQuery }) => !firstQuery.data,
options: ({firstQuery}) => ({
variables: {
var1: firstQuery.data.someQuery.someValue
}
})
})
)(withRouter(TestPage))
Notice that we use skip to skip the second query unless we actually have data from the first query to work with.
Using the Query Component
If you're using the Query component, you can also utilize the skip property, although you also have the option to return something else (like null or a loading indicator) inside the first render props function:
<Query query={firstQuery}>
{({ data: { someQuery: { someValue } = {} } = {} }) => (
<Query
query={secondQuery}
variables={{var1: someValue}}
skip={someValue === undefined}
>
{({ data: secondQueryData }) => (
// your component here
)}
</Query>
Using the useQuery Hook
You can also use skip with the useQuery hook:
const { data: { someQuery: { someValue } = {} } = {} } = useQuery(firstQuery)
const variables = { var1: someValue }
const skip = someValue === undefined
const { data: secondQueryData } = useQuery(secondQuery, { variables, skip })
Mutations
Unlike queries, mutations involve specifically calling a function in order to trigger the request. This function returns a Promise that will resolve with the results of the mutation. That means, when working with mutations, you can simply chain the resulting Promises:
const [doA] = useMutation(MUTATION_A)
const [doB] = useMutation(MUTATION_B)
// elsewhere
const { data: { someValue } } = await doA()
const { data: { someResult } } = await doB({ variables: { someValue } })
For anyone using react apollo hooks the same approach works.
You can use two useQuery hooks and pass in the result of the first query into the skip option of the second,
example code:
const AlertToolbar = ({ alertUid }: AlertToolbarProps) => {
const authenticationToken = useSelectAuthenticationToken()
const { data: data1 } = useQuery<DataResponse>(query, {
skip: !authenticationToken,
variables: {
alertUid,
},
context: makeContext(authenticationToken),
})
const { data: data2, error: error2 } = useQuery<DataResponse2>(query2, {
skip:
!authenticationToken ||
!data1 ||
!data1.alertOverview ||
!data1.alertOverview.deviceId,
variables: {
deviceId:
data1 && data1.alertOverview ? data1.alertOverview.deviceId : null,
},
context: makeContext(authenticationToken),
})
if (error2 || !data2 || !data2.deviceById || !data2.deviceById.id) {
return null
}
const { deviceById: device } = data2
return (
<Toolbar>
...
// do some stuff here with data12

Admin on rest - implementing aor-realtime

I'm having a real hard time understanding how to implement aor-realtime (trying to use it with firebase; reads only, no write).
The first place I get stuck: This library generates a saga, right? How do I connect that with a restClient/resource? I have a few custom sagas that alert me on errors, but there is a main restClient/resource backing those. Those sagas just handles some side-effects. In this case, I just don't understand what the role of the client is, and how it interacts with the generated saga (or visa-versa)
The other question is with persistence: Updates stream in and the initial set of records is not loaded in one go. Should I be calling observer.next() with each update? or cache the updated records and call next() with the entire collection to-date.
Here's my current attempt at doing the later, but I'm still lost with how to connect it to my Admin/Resource.
import realtimeSaga from 'aor-realtime';
import { client, getToken } from '../firebase';
import { union } from 'lodash'
let cachedToken
const observeRequest = path => (type, resource, params) => {
// Filtering so that only chats are updated in real time
if (resource !== 'chat') return;
let results = {}
let ids = []
return {
subscribe(observer) {
let databaseRef = client.database().ref(path).orderByChild('at')
let events = [ 'child_added', 'child_changed' ]
events.forEach(e => {
databaseRef.on(e, ({ key, val }) => {
results[key] = val()
ids = union([ key ], ids)
observer.next(ids.map(id => results[id]))
})
})
const subscription = {
unsubscribe() {
// Clean up after ourselves
databaseRef.off()
results = {}
ids = []
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
},
};
};
export default path => realtimeSaga(observeRequest(path));
How do I connect that with a restClient/resource?
Just add the created saga to the custom sagas of your Admin component.
About the restClient, if you need it in your observer, then pass it the function which return your observer as you did with path. That's actually how it's done in the readme.
Should I be calling observer.next() with each update? or cache the updated records and call next() with the entire collection to-date.
It depends on the type parameter which is one of the admin-on-rest fetch types:
CRUD_GET_LIST: you should return the entire collection, updated
CRUD_GET_ONE: you should return the resource specified in params (which should contains its id)
Here's the solution I came up with, with guidance by #gildas:
import realtimeSaga from "aor-realtime";
import { client } from "../../../clients/firebase";
import { union } from "lodash";
const observeRequest = path => {
return (type, resource, params) => {
// Filtering so that only chats are updated in real time
if (resource !== "chats") return;
let results = {}
let ids = []
const updateItem = res => {
results[res.key] = { ...res.val(), id: res.key }
ids = Object.keys(results).sort((a, b) => results[b].at - results[a].at)
}
return {
subscribe(observer) {
const { page, perPage } = params.pagination
const offset = perPage * (page - 1)
const databaseRef = client
.database()
.ref(path)
.orderByChild("at")
.limitToLast(offset + perPage)
const notify = () => observer.next({ data: ids.slice(offset, offset + perPage).map(e => results[e]), total: ids.length + 1 })
databaseRef.once('value', snapshot => {
snapshot.forEach(updateItem)
notify()
})
databaseRef.on('child_changed', res => {
updateItem(res)
notify()
})
const subscription = {
unsubscribe() {
// Clean up after ourselves
databaseRef.off();
// Notify the saga that we cleaned up everything
observer.complete();
}
};
return subscription;
}
};
}
};
export default path => realtimeSaga(observeRequest(path));

Redux architecture: multiple items editable on same page with new item form

I have list of items on single screen. The list is fetched from redux store and rendered as set of form components. At the bottom of list I have "Add new" button.
In my initial implementation I have used container component with setState({newItem: {...}). This has side effect of view flickering when newItem is cleared and new item is added to redux items.
{items.map((item, index) =>
<ItemForm
key={index}
formKey={index.toString()}
initialValues={item}
submitItem={this.handleUpdateItem.bind(this)}
removeItem={() => this.handleRemoveItem(item.id)}
/>
)}
{newItem &&
<ItemForm
key="new"
formKey="new"
initialValues={newItem}
submitItem={this.handleCreateItem.bind(this)}
removeItem={() => this.handleRemoveNewItem()}
/>
}
I think better way to implement this would be not to use newItem and instead handle the new item inside redux. But in that case I have to change the actions/reducers API, in particular the CREATE_ITEM would no longer append item but rather updates the last item in store.
Is there some way I can reduce the flickering or do you have some suggestions how to do it with redux? In particular - what set of actions / reducers should I use to model better solution? Should I rather make API calls in container and simplify the redux actions?
The complexity of the actions/reducers API for me comes from mixing persisted (requires id to make request) and not persisted objects (requires index to find) in redux.
Here is my redux module:
import axios from 'axios';
import * as _ from 'lodash';
export const FETCH_ITEMS = 'items/FETCH_ITEMS';
export const CREATE_ITEM = 'items/CREATE_ITEM';
export const REMOVE_ITEM = 'items/REMOVE_ITEM';
export const UPDATE_ITEM = 'items/UPDATE_ITEM';
const initialState = {
items: []
};
export default function reducer(state = initialState, action) {
let index;
switch (action.type) {
case FETCH_ITEMS:
return {
...state,
items: action.items
};
case CREATE_ITEM: // would become "last item UPDATE_ITEM"
return {
...state,
items: [
...state.items,
action.item
]
};
case REMOVE_ITEM:
index = _.findIndex(state.items, {id: action.id});
return {
...state,
items: [
...state.items.slice(0, index),
action.item,
...state.items.slice(index + 1)
]
};
case UPDATE_ITEM:
index = _.findIndex(state.items, {id: action.item.id});
return {
...state,
items: [
...state.items.slice(0, index),
action.item,
...state.items.slice(index + 1)
]
};
default:
return state;
}
}
export function fetchItems() {
return function (dispatch) {
return axios.get('/api/items')
.then(res => {
const items = res.data;
dispatch({type: FETCH_ITEMS, items});
})
}
}
export function createItem(item) {
return function (dispatch) {
return axios.post('/api/items', item)
.then(res => {
const item = res.data.item;
dispatch({type: CREATE_ITEM, item});
})
}
}
export function removeItem(id) {
return function (dispatch) {
return axios.delete(`/api/items/${id}`)
.then(() => {
dispatch({type: REMOVE_ITEM, id});
});
}
}
export function updateItem(item) {
const {id, ...rest} = item;
return function (dispatch) {
return axios.put(`/api/items/${id}`, rest)
.then(res => {
const item = deserializeItem(res.data.item);
dispatch({type: UPDATE_ITEM, item});
})
}
}

Resources