useState depending on other state - react-hooks

I have this useSiren hook that should update its state with the incoming json argument but it doesnt.
On the first call the json is an empty object, because the fetch effect has not been run yet.
On the second call its also an empty object (triggered by loading getting set to true in App)
And on the third call its filled with valid data. However, the valid data is not applied. The state keeps its initial value.
I guess somehow setSiren must be called to update it, since initial state can only be set once. But how would I do that? Who should call `setSiren?
import { h, render } from 'https://unpkg.com/preact#latest?module';
import { useEffect, useState, useCallback } from 'https://unpkg.com/preact#latest/hooks/dist/hooks.module.js?module';
import htm from "https://unpkg.com/htm#latest/dist/htm.module.js?module";
const html = htm.bind(h);
function useFetch({
method = "GET",
autoFetch = true,
href,
body
}) {
const [loading, setLoading] = useState(false)
const [error, setError] = useState()
const [response, setResponse] = useState()
const [isCancelled, cancel] = useState()
const [json, setJson] = useState({})
const sendRequest = async payload => {
try {
setLoading(true)
setError(undefined)
const response = await fetch(href.replace("http://", "https://"), {
method
})
const json = await response.json()
if (!isCancelled) {
setJson(json)
setResponse(response)
}
return json
} catch (err) {
if (!isCancelled) {
setError(err)
}
throw err
} finally {
setLoading(false)
}
}
if (autoFetch) {
useEffect(() => {
sendRequest(body)
return () => cancel(true)
}, [])
}
return [{
loading,
response,
error,
json
},
sendRequest
]
}
function useSiren(json) {
const [{ entities = [], actions = [], links, title }, setSiren] = useState(json)
const state = (entities.find(entity => entity.class === "state")) || {}
return [
{
title,
state,
actions
},
setSiren
]
}
function Action(props) {
const [{ loading, error, json }, sendRequest] = useFetch({ autoFetch: false, href: props.href, method: props.method })
const requestAndUpdate = () => {
sendRequest().then(props.onRefresh)
}
return (
html`
<button disabled=${loading} onClick=${requestAndUpdate}>
${props.title}
</button>
`
)
}
function App() {
const [{ loading, json }, sendRequest] = useFetch({ href: "https://restlr.io/toggle/0" })
const [{ state, actions }, setSiren] = useSiren(json)
return (
html`<div>
<div>State: ${loading ? "Loading..." : (state.properties && state.properties.value)}</div>
${actions.map(action => html`<${Action} href=${action.href} title=${action.title || action.name} method=${action.method} onRefresh=${setSiren}/>`)}
<button disabled=${loading} onClick=${sendRequest}>
REFRESH
</button>
</div>
`
);
}
render(html`<${App}/>`, document.body)

Maybe what you want to do is to update the siren state when the json param changes? You can use a useEffect to automatically update it.
function useSiren(json) {
const [{ entities = [], actions = [], links, title }, setSiren] = useState(json)
useEffect(() => { // here
setSiren(json)
}, [json])
const state = (entities.find(entity => entity.class === "state")) || {}
return [
{
title,
state,
actions
},
setSiren
]
}

The pattern mentioned by #awmleer is packaged in use-selector:
import { useSelectorValue } from 'use-selector';
const { entities=[], actions=[], title} = json;
const siren = useSelectorValue(() => ({
state: entities.find(entity => entity.class === 'state') || {},
actions,
title
}), [json]);
Disclosure I'm author and maintainer of use-selector

Related

how to prevent api call from redux toolkit because its calling everytime on comp renders

ProfilePic.js
Due to useeffect on each component rendering API call happen.
How to solve this issue?
useEffect(() => {
dispatch(getImages())
}, [dispatch])
The API call is happening in ProfilePicSlice.js file below:
or how to add if condition in if condition, refer below code
ProfilePicSlice.js
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit'
import { IMAGE_API, ACCESS_KEY } from "../../app/utils/constant";
export const getImages = createAsyncThunk('imageList/images',
(_, { getState }) => {
const store = getState();
const topic = store.users[0].topic;
const otherTopic = store.users[0].otherTopic;
if(store === null){
console.log('api call')
}else{
console.log('no api call')
}
if(topic === 'Other'){
return fetch(`${IMAGE_API}${otherTopic}${ACCESS_KEY}`)
.then((res) => res.json());
}else{
return fetch(`${IMAGE_API}${topic}${ACCESS_KEY}`)
.then((res) => res.json());
}
})
const ProfilePicSlice = createSlice({
name: 'imageList',
initialState: {
images: [],
selectedImage: {},
loading: false,
},
extraReducers: (builder) => {
builder.addCase(getImages.fulfilled, (state, action) => {
state.loading = false;
state.images.push(action.payload);
console.log(action.payload)
})
},
reducers: {
selectedImage: (state, action) => {
state.selectedImage = action.payload;
},
removeImage: (state, action) => {
const itemId = action.payload;
state.images = state.images.filter((item) => item.id !== itemId);
}
},
});
export const { selectedImage, removeImage } = ProfilePicSlice.actions
export default ProfilePicSlice.reducer

Cannot see data in view page source even though Cache of Apollo Client have data

I don't know why in another page, I use this way just different query and I can see data in view page source, but in this page , it not work. I wondering it cause I use localStorage value as params, i don't think problem come from query.
interface Props {
__typename?: 'ProductOfBill';
amount: number,
name: string,
totalPrice: number,
type: string,
unitPrice: number,
}
const Cart = () => {
const [products,setProducts] = useState<Props[]>([])
const { data } = useGetSomeProductQuery({
variables: { productList: productListForBill()},
notifyOnNetworkStatusChange: true
});
useEffect(() =>{
if(data?.getSomeProduct){
setProducts(data.getSomeProduct)
}
},[data])
return (
<>
...
</>
);
};
export const getStaticProps: GetStaticProps = async () => {
const apolloClient = initializeApollo();
await apolloClient.query<GetSomeProductQuery>({
query: GetSomeProductDocument,
variables: { productList: productListForBill() },
});
return addApolloState(apolloClient, {
props: {},
});
};
export default Cart;
I get localStorage value from this method.
export const productListForBill = () : GetProductForBill[] =>{
const returnEmtpyArray : GetProductForBill[] = []
if(typeof window !== "undefined"){
if(localStorage.getItem("products"))
{
const tempProduct = JSON.parse(localStorage.getItem("products") || "")
if(Array.isArray(tempProduct)){
return tempProduct
}
}
}
return returnEmtpyArray
}
and I custom Apollo Client like doc of Nextjs in github
import { useMemo } from 'react'
import { ApolloClient, HttpLink, InMemoryCache, NormalizedCacheObject } from '#apollo/client'
import merge from 'deepmerge'
import isEqual from 'lodash/isEqual'
export const APOLLO_STATE_PROP_NAME = '__APOLLO_STATE__'
interface IApolloStateProps {
[APOLLO_STATE_PROP_NAME]?: NormalizedCacheObject
}
let apolloClient : ApolloClient<NormalizedCacheObject>
function createApolloClient() {
return new ApolloClient({
//type of "window"=== undifined
ssrMode: true,
link: new HttpLink({
uri: "http://localhost:4000/graphql",
credentials: "include",
}),
cache: new InMemoryCache()
)}
}
export function initializeApollo(initialState : NormalizedCacheObject | null = null) {
const _apolloClient = apolloClient ?? createApolloClient()
if (initialState) {
const existingCache = _apolloClient.extract()
cache
const data = merge(existingCache, initialState, {
arrayMerge: (destinationArray, sourceArray) => [
...sourceArray,
...destinationArray.filter((d) =>
sourceArray.every((s) => !isEqual(d, s))
),
],
})
_apolloClient.cache.restore(data)
}
if (typeof window === 'undefined') return _apolloClient
if (!apolloClient) apolloClient = _apolloClient
return _apolloClient
}
export function addApolloState(client : ApolloClient<NormalizedCacheObject>, pageProps: { props: IApolloStateProps }) {
if (pageProps?.props) {
pageProps.props[APOLLO_STATE_PROP_NAME] = client.cache.extract()
}
return pageProps
}
export function useApollo(pageProps : IApolloStateProps) {
const state = pageProps[APOLLO_STATE_PROP_NAME]
const store = useMemo(() => initializeApollo(state), [state])
return store
}
Answering
Cannot see data in view page source even though Cache of Apollo Client have data
These are client side methods, value will not be visible in view source but in evaluated source, look in the elements panel in chrome.

How to load AJAX and update state in componentDidUpdate?

I have a component that renders a list of credit cards. It receives a contract as a prop. A contract id can have multiple related credit cards. This list is fetched from the API via AJAX. The component can be visible or hidden on screen.
I fetch the list of credit cards via AJAX in state cards which is a tuple (key/value): [number, ICreditCardRecord[]]. That way I can keep track of which contract id the list I have in state belongs to.
So in componentDidUpdate I check if the component is not hidden and if the selected contract matches the one on which I saved the list. If not, I fetch the list again for the new contract.
Problem is, it triggers an endless loop. I read that it's not possible to setState inside componentDidUpdate as it triggers a re-render which triggers the function again which ... (endless loop).
So how can I do this? BTW I also use state to manage whether to show loading overlay or not (isLoading: boolean). I update this state prop when ever I start/stop loading.
import React, { Component } from 'react'
import { IApiVehicleContract, ICreditCardRecord } from '#omnicar/sam-types'
import * as api from 'api/api'
import { WithStyles, withStyles } from '#material-ui/core'
import CreditCard from 'components/customer/Contract/Details/CreditCard'
import NewCreditCard from 'components/customer/Contract/Details/NewCreditCard'
import AddCardDialog from 'components/customer/Contract/Details/AddCardDialog'
import { AppContext } from 'store/appContext'
import LoadingOverlay from 'components/LoadingOverlay'
import styles from './styles'
import alertify from 'utils/alertify'
import { t } from '#omnicar/sam-translate'
import { loadScript } from 'utils/script'
interface IProps extends WithStyles<typeof styles> {
contract: IApiVehicleContract | undefined
hidden: boolean
}
interface IState {
cards: [number, ICreditCardRecord[]]
showAddCreditCardDialog: boolean
isStripeLoaded: boolean
isLoading: boolean
}
class CustomerContractDetailsTabsCreditCards extends Component<IProps, IState> {
public state = {
cards: [0, []] as [number, ICreditCardRecord[]],
showAddCreditCardDialog: false,
isStripeLoaded: false,
isLoading: false,
}
public componentDidUpdate(prevProps: IProps, prevState: IState) {
const { cards } = this.state
const { contract, hidden } = this.props
// selected contract changed
const changed = contract && contract.serviceContractId !== cards[0]
// visible and changed
if (!hidden && changed) {
this.getCreditCards()
console.log('load')
}
}
public async componentDidMount() {
// load stripe
const isStripeLoaded = await loadScript('https://js.stripe.com/v3/', 'stripe-payment')
this.setState({ isStripeLoaded: !!isStripeLoaded })
}
public render() {
const { classes, contract, hidden } = this.props
const { cards, showAddCreditCardDialog, isLoading } = this.state
return (
<div className={`CustomerContractDetailsTabsCreditCards ${classes.root} ${hidden && classes.hidden}`}>
<React.Fragment>
<ul className={classes.cards}>
{cards[1].map(card => (
<CreditCard
className={classes.card}
key={card.cardId}
card={card}
onDeleteCard={this.handleDeleteCard}
onMakeCardDefault={this.handleMakeDefaultCard}
/>
))}
<NewCreditCard className={classes.card} onToggleAddCreditCardDialog={this.toggleAddCreditCardDialog} />
</ul>
<AppContext.Consumer>
{({ stripePublicKey }) => {
return (
<AddCardDialog
open={showAddCreditCardDialog}
onClose={this.toggleAddCreditCardDialog}
contract={contract!}
onAddCard={this.handleAddCard}
stripePublicKey={stripePublicKey}
isLoading={isLoading}
/>
)
}}
</AppContext.Consumer>
</React.Fragment>
<LoadingOverlay open={isLoading} />
</div>
)
}
private getCreditCards = async () => {
const { contract } = this.props
if (contract) {
// this.setState({ isLoading: true })
const res = await api.getContractCreditCards(contract.prettyIdentifier)
debugger
if (res) {
if (res.errorData) {
// this.setState({ isLoading: false })
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
console.error(res.errorData.message)
return
}
// sort list: put active first
const creditCards: ICreditCardRecord[] = res.data!.sort((a, b) => +b.isDefault - +a.isDefault)
const cards: [number, ICreditCardRecord[]] = [contract.serviceContractId, creditCards]
debugger
this.setState({ cards, isLoading: false })
}
}
}
private handleDeleteCard = async (cardId: string) => {
const { contract } = this.props
// show spinner
this.setState({ isLoading: true })
const req = await api.deleteCreditCard(contract!.prettyIdentifier, cardId)
if (req && (req.errorData || req.networkError)) {
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
// hide spinner
this.setState({ isLoading: false })
return console.error(req.errorData || req.networkError)
}
// remove card from list
const creditCards = this.state.cards[1].filter(card => card.cardId !== cardId)
const cards: [number, ICreditCardRecord[]] = [this.state.cards[0], creditCards]
// update cards list + hide spinner
this.setState({ isLoading: false, cards })
// notify user
alertify.success(t('Credit card has been deleted'))
}
private handleMakeDefaultCard = async (cardId: string) => {
const { contract } = this.props
// show spinner
this.setState({ isLoading: true })
const req = await api.makeCreditCardDefault(contract!.prettyIdentifier, cardId)
if (req && (req.errorData || req.networkError)) {
alertify.warning(t('A problem occured. Please contact OmniCar If the problem persists...'))
// hide spinner
this.setState({ isLoading: false })
return console.error(req.errorData || req.networkError)
}
// show new card as default
const creditCards = this.state.cards[1].map(card => {
const res = { ...card }
if (card.cardId !== cardId) {
res.isDefault = false
} else {
res.isDefault = true
}
return res
})
const cards: [number, ICreditCardRecord[]] = [this.state.cards[0], creditCards]
// update cards list + hide spinner
this.setState({ isLoading: false, cards })
// notify user
alertify.success(t('Credit card is now default'))
}
private toggleAddCreditCardDialog = () => {
this.setState({ showAddCreditCardDialog: !this.state.showAddCreditCardDialog })
}
private appendNewCardToList = (data: any) => {
const {cards: cardsList} = this.state
let creditCards = cardsList[1].map(card => {
return { ...card, isDefault: false }
})
creditCards = [data, ...creditCards]
const cards: [number, ICreditCardRecord[]] = [cardsList[0], creditCards]
this.setState({ cards })
// notify user
alertify.success(t('Credit card has been added'))
}
private handleAddCard = (stripe: stripe.Stripe) => {
// show spinner
this.setState({ isLoading: true })
const stripeScript = stripe as any
stripeScript.createToken().then(async (result: any) => {
if (result.error) {
// remove spinner
this.setState({ isLoading: false })
// notify user
alertify.warning(t('An error occurred... Please contact OmniCar if the problem persists. '))
return console.error(result.error)
}
// add credit card
const prettyId = this.props.contract ? this.props.contract.prettyIdentifier : ''
const req = await api.addCreditCard({ cardToken: result.token.id, isDefault: true }, prettyId)
if (req.errorData) {
alertify.warning(t('An error occurred while trying to add credit card'))
return console.error(req.errorData)
}
// remove spinner and close dialog
this.setState({ isLoading: false }, () => {
this.toggleAddCreditCardDialog()
this.appendNewCardToList(req.data)
})
})
}
}
export default withStyles(styles)(CustomerContractDetailsTabsCreditCards)

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.

Graphql-js subscriptions unit tests not working as expected

I have written integration tests for graphql-js subscriptions, which are showing weird behavior.
My graphq-js subscription works perfectly in GraphiQL. But when the same subscriptions is called from unit test, it fails.
Ggraphql-Js object, with resolve function and subscribe function
return {
type: outputType,
args: {
input: {type: new GraphQLNonNull(inputType)},
},
resolve(payload, args, context, info) {
const clientSubscriptionId = (payload) ? payload.subscriptionId : null;
const object = (payload) ? payload.object : null;
var where = null;
var type = null;
var target = null;
if (object) {
where = (payload) ? payload.object.where : null;
type = (payload) ? payload.object.type : null;
target = (payload) ? payload.object.target : null;
}
return Promise.resolve(subscribeAndGetPayload(payload, args, context, info))
.then(payload => ({
clientSubscriptionId, where, type, target, object: payload.data,
}));
},
subscribe: withFilter(
() => pubSub.asyncIterator(modelName),
(payload, variables, context, info) => {
const subscriptionPayload = {
clientSubscriptionId: variables.input.clientSubscriptionId,
remove: variables.input.remove,
create: variables.input.create,
update: variables.input.update,
opts: variables.input.options,
};
subscriptionPayload.model = model;
try {
pubSub.subscribe(info.fieldName, null, subscriptionPayload);
} catch (ex) {
console.log(ex);
}
return true;
}
),
};
Subscription query
subscription {
Customer(input: {create: true, clientSubscriptionId: 112}) {
customer {
id
name
age
}
}
}
Mutation query
mutation {
Customer {
CustomerCreate (input:{data:{name:"Atif 50", age:50}}) {
obj {
id
name
}
}
}
}
Integration Test
'use strict';
const ws = require('ws');
const { SubscriptionClient } = require('subscriptions-transport-ws');
const { ApolloClient } = require('apollo-client');
const { HttpLink } = require('apollo-link-http');
const { InMemoryCache } = require('apollo-cache-inmemory');
const Promise = require('bluebird');
const expect = require('chai').expect;
const chai = require('chai').use(require('chai-http'));
const server = require('../server/server');
const gql = require('graphql-tag');
let apollo;
let networkInterface;
const GRAPHQL_ENDPOINT = 'ws://localhost:5000/subscriptions';
describe('Subscription', () => {
before(async () => {
networkInterface = new SubscriptionClient(
GRAPHQL_ENDPOINT, { reconnect: true }, ws);
apollo = new ApolloClient({
networkInterface ,
link: new HttpLink({ uri: 'http://localhost:3000/graphql' }),
cache: new InMemoryCache()
});
});
after(done => {
networkInterface.close() ;
});
it('subscription', async () => {
const client = () => apollo;
// SUBSCRIBE and make a promise
const subscriptionPromise = new Promise((resolve, reject) => {
client().subscribe({
query: gql`
subscription {
Customer(input: {create: true,
clientSubscriptionId: 112,
options: {where: {age: 50}}}) {
customer {
name
}
}
}
`
}).subscribe({
next: resolve,
error: reject
});
});
let execGraphQL;
// MUTATE
await execGraphQL(
`mutation {
Customer {
CustomerCreate (input:{data:{name:"Atif 21", age:50}}) {
obj {
id
name
}
}
}
}`
);
// ASSERT SUBSCRIPTION RECEIVED EVENT
expect(await subscriptionPromise).to.deep.equal({});
});
});
Issue Here
When test in run, payload in the resolve function contains global data, where as it should contain the subscription payload. So the code breaks.

Resources