How to properly hook up Apollo Client to Redux in React Native - reactjs

I've been researching this for the past couple hours, and I am now extremely close to solving this. Everything seems to be working now, but I do not have this.props.mutate in my component that has the Apollo HOC wrapping it.
I wired Apollo up as a reducer, so in my component, I would expect to see this.props.apollo.mutate available, but it's not there.
This is all that seems to be provided currently:
console.log(this.props.apollo)
{"data":{},"optimistic":[],"reducerError":null}
Here is how it is hooked up:
./src/apolloClient.js
import { AsyncStorage } from 'react-native'
import { ApolloClient, createNetworkInterface } from 'react-apollo'
const networkInterface = createNetworkInterface({
uri: 'http://localhost:8000/graphql',
opts: {
credentials: 'same-origin',
mode: 'cors'
}
})
// networkInterface is half baked as I haven't got this far yet
networkInterface.use([{
async applyMiddleware(req, next) {
if (!req.options.headers) req.options.headers = {}
const token = await AsyncStorage.getItem('token')
req.options.headers.authorization = token ? token : null
next()
}
}])
const client = new ApolloClient({
networkInterface,
dataIdFromObject: (o) => o.id
})
export default client
./src/reducers.js
import client from './apolloClient'
export default combineReducers({
apollo: client.reducer(),
nav: navReducer,
signup: signupReducer,
auth: loginReducer,
})
./src/App.js
import store from './store'
import client from './apolloClient'
const Root = () => {
return (
<ApolloProvider store={store} client={client}>
<RootNavigationStack />
</ApolloProvider>
)
}
export default Root
Oh, and here's the bottom of my Login component (also fairly half-baked):
export default compose(
connect(mapStateToProps, {
initiateLogin,
toggleRememberMe
}),
withFormik({
validate,
validateOnBlur: true,
validateOnChange: true,
handleSubmit: ({ tel, password }, { props }) => {
props.apollo.mutate({
variables: { tel, password }
})
.then((res) => {
alert(JSON.stringify(res))
//const token = res.data.login_mutation.token
//this.props.signinUser(token)
//client.resetStore()
})
.catch((err) => {
alert(JSON.stringify(err))
//this.props.authError(err)
})
//props.initiateLogin({ tel, password })
}
}),
graphql(LOGIN_MUTATION, { options: { fetchPolicy: 'network-only' } })
)(LoginForm)
It feels like I need an action creator and to manually map it to my component. What do I need to do to run this loosely shown mutation LOGIN_MUTATION onSubmit?
I'm currently confused by the fact this.props.apollo has Apollo's data in it, but there is no mutate.
I don't see the solution here: http://dev.apollodata.com/react/mutations.html or maybe I do -- is this what I need to be looking at?
const NewEntryWithData = graphql(submitRepository, {
props: ({ mutate }) => ({
submit: (repoFullName) => mutate({ variables: { repoFullName } }),
}),
})(NewEntry)
I'd like to get it to the point where the component can call the mutation when it needs to. I'd also like it to be available on this.props.something so I can call it from Formik's handleSubmit function, but I am open to suggestions that enable the best declarative scalability.
[edit] Here is the code that I am considering solved:
./src/apolloClient.js
This file was scrapped.
./src/reducers.js
I removed the Apollo reducer and client reference.
./src/App.js
I put the Apollo Client inside the root component. I got this technique from Nader Dabit's Medium post. He illustrates this in a GitHub repo:
https://github.com/react-native-training/apollo-graphql-mongodb-react-native
https://medium.com/react-native-training/react-native-with-apollo-part-2-apollo-client-8b4ad4915cf5
Here is how it looks implemented:
const Root = () => {
const networkInterface = createNetworkInterface({
uri: 'http://localhost:8000/graphql',
opts: {
credentials: 'same-origin',
mode: 'cors'
}
})
networkInterface.use([{
async applyMiddleware(req, next) {
try {
if (!req.options.headers) req.options.headers = {}
const token = await AsyncStorage.getItem('token')
req.options.headers.authorization = token || null
next()
} catch (error) {
next()
}
}
}])
const client = new ApolloClient({
networkInterface,
dataIdFromObject: (o) => o.id
})
return (
<ApolloProvider store={store} client={client}>
<RootNavigationStack />
</ApolloProvider>
)
}
export default Root

When you use compose, the order of your HOCs matters. In your code, the props added by your first HOC (connect) are available to all the HOCs after it (withFormik and graphql). The props added by withFormik are only available to graphql. The props added by graphql are available to neither of the other two HOCs (just the component itself).
If you rearrange the order to be compose -> graphql -> withFormik then you should have access to props.mutate inside withFormik.
Additionally, while you can integrate Redux and Apollo, all this does is prevent you from having two separate stores. Passing an existing store to Apollo is not going to change the API for the graphql HOC. That means, regardless of what store you're using, when you correctly use the HOC, you will still get a data prop (or a mutate prop for mutations).
While integrating Apollo with Redux does expose Apollo's store to your application, you should still use Apollo like normal. In most cases, that means using the graphql HOC and utilizing data.props and data.mutate (or whatever you call those props if you pass in a name through options).
If you need to call the Apollo client directly, then use withApollo instead -- this exposes a client prop that you can then use. The apollo prop that connect exposes in your code is just the store used by Apollo -- it's not the actual client, so it will not have methods like mutate available to it. In most cases, though, there's no reason to go with withApollo over graphql.

Related

How to share redux state client-side and props server-side in Next JS

I'm a newbie with Next JS.
I use Next JS and Redux.
I have a short code below:
const AdminContainer = (props) => {
return (
<AdminMasterView>
<DashboardView studentList={props.studentListServer}/>
</AdminMasterView>
)
}
export const getStaticProps = (async () => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList
});
const mapDispatchToProps = {
getStudentRegisterAction
};
export default connect(mapStateToProps, mapDispatchToProps)(AdminContainer);
I also have studentList (array type) props is declare in Redux. I want to use it to pass data because I have many tasks to do with data such as filter, order,...
Is there any way to use studentList like this and my app still is server rendering first time.
If I dispatch studentListServer to studentList, it still work. But my app isn't server rendering.
<DashboardView studentList={props.studentList}/>
Or easier, I'll check to use props.studentList for client-side and props.studentListServer for server-side. But I think it's not good.
Thank you so much!
You could use the next-redux-wrapper package. It allows to sync a Redux state on server and client. Consider the example:
export const getStaticProps = wrapper.getStaticProps(async ({ store }) => {
let response = await db.getInstance().query('SELECT * FROM student_register;');
// dispatch the action that saves the data
store.dispatch({ type: 'SET_STUDENTS', payload: response });
return {
props: {
studentListServer: response
}, // will be passed to the page component as props
}
})
wrapper.getStaticProps wraps your getStaticProps function with the new parameter store that is a Redux store in fact.
Action with type SET_STUDENTS sets the student list on a server side. When Next.js generates the page, it will save this data in static JSON. So when the page opens on client side, next-redux-wrapper recreates a state dispatching HYDRATE action with saved on a build time static JSON that you can use to restore the studentInfoReducers reducer.
E.g. in your reducer you should implement something like:
import { HYDRATE } from 'next-redux-wrapper';
const initialState = { studentList: [] };
// studentInfoReducers reducer
function reducer(state = initialState, action) {
// this sets your student list
if (action.type === 'SET_STUDENTS') {
return {
...state,
studentList: action.payload,
};
}
// this rehydrates your store from server on a client
if (action.type === HYDRATE) {
return action.payload.studentInfoReducers;
}
return state;
}
So afterwards you should have a valid synced state on client and server at the same time:
const mapStateToProps = state => ({
studentList: state.studentInfoReducers.studentList // works on server and client
});
Let me know if you have any questions, next-redux-wrapper can be tricky from a first look.
You don't need to use Redux for that.
Using just cookies you can achieve bidirectional communication, see https://maxschmitt.me/posts/next-js-cookies/
Another example:
Client to Server: manually set a cookie in the client side and then read it in the server with req.headers.cookie or some library like 'cookie'
Server to Client: just read the cookie, and return what you need as a regular prop or update the cookie.
import { useState, useEffect } from "react";
import Cookie from "js-cookie";
import { parseCookies } from "../lib/parseCookies";
const Index = ({ initialRememberValue = true }) => {
const [rememberMe, setRememberMe] = useState(() =>
JSON.parse(initialRememberValue)
);
useEffect(() => {
//save/create the cookie with the value in the client
Cookie.set("rememberMe", JSON.stringify(rememberMe));
}, [rememberMe]);
return (
<div>
remember me
<input
type="checkbox"
value={rememberMe}
checked={rememberMe}
onChange={e => setRememberMe(e.target.checked)}
/>
</div>
);
};
Index.getInitialProps = ({ req }) => {
//read the cookie on the server
const cookies = parseCookies(req); //parseCookies is a simple custom function you can find
return {
//send the value as a regular prop
initialRememberValue: cookies.rememberMe
};
};
export default Index;
Reference: https://github.com/benawad/nextjs-persist-state-with-cookie/blob/master/pages/index.js

Safe way to initialize Apollo Client upon asynchronous token

I'm writing an app that uses Apollo Client to make graphql requests against a MongoDB Realm database.
This is the abstract structure of it:
<MongoContext>
<ApolloContext>
<App/>
</ApolloContext>
</MongoContext>
The top level component handles user authentication and provides a context. The next component down initiates Apollo Client and the caching logic and sets the context to the whole app.
The expected data flow is shown in the diagram on this page. The default behavior for a useQuery is that Apollo:
Tries to fetch from cache;
Tries to fetch from server; If successful, saves to cache.
My goal is to achieve offline capability. So, referring to the diagram again, the very first query should be resolved by the cache when it holds the data. Apollo's default cache mechanism is in memory, so I'm using apollo-cache-persist to cache it to localStorage.
More specifically, these are the conditions required:
The app should be responsive upon start.
At first render, the app is either offline or hasn't authenticated yet
Therefore it must read from cache, if available
If there's no cache, don't make any requests to the server (they'd all fail)
If the user is online, the app should get the authentication token for the requests
The token is requested asynchronously
While the token is unknown, read from the cache only (As 1.2 above)
When the token is known, use the data flow described above
My main problems are specifically with 1.2 and 2.2 above. I.e. preventing Apollo from making requests to the server when we already know it will fail.
I was also looking for a global solution, so modifying individual queries with skip or useLazyQuery aren't options. (And I'm not even sure that would work - I still needed the queries to be executed against the cache.)
Code:
ApolloContext component:
import * as React from 'react';
import {
ApolloClient,
InMemoryCache,
ApolloProvider,
createHttpLink,
NormalizedCacheObject,
} from '#apollo/client';
import { setContext } from '#apollo/client/link/context';
import { persistCache } from 'apollo-cache-persist';
import { PersistentStorage } from 'apollo-cache-persist/types';
const ApolloContext: React.FC = ({ children }) => {
// this hook gets me the token asynchronously
// token is '' initially but eventually resolves... or not
const { token } = useToken();
const cache = new InMemoryCache();
const [client, setClient] = React.useState(createApolloClient(token, cache))
// first initialize the client without the token, then again upon receiving it
React.useEffect(() => {
const initPersistCache = async () => {
await persistCache({
cache,
storage: capacitorStorageMethods,
debug: true,
});
};
const initApollo = async () => {
await initPersistCache();
setClient(createApolloClient(token, cache));
};
if (token) {
initApollo();
} else {
initPersistCache();
}
}, [token]);
console.log('id user', id, user);
return <ApolloProvider client={client}>{children}</ApolloProvider>;
};
function createApolloClient(
token: string,
cache: InMemoryCache
) {
const graphql_url = `https://realm.mongodb.com/api/client/v2.0/app/${realmAppId}/graphql`;
const httpLink = createHttpLink({
uri: graphql_url,
});
const authorizationHeaderLink = setContext(async (_, { headers }) => {
return {
headers: {
...headers,
Authorization: `Bearer ${token}`,
},
};
});
return new ApolloClient({
link: authorizationHeaderLink.concat(httpLink),
cache,
});
}
What I've tried:
After attempting many different things. I found something that works, but it looks terrible. The trick is to give Apollo a custom fetch that rejects all requests when the user is not logged in:
const customFetch = (input: RequestInfo, init?: RequestInit | undefined) => {
return user.isLoggedIn
? fetch(input, init)
: Promise.reject(new Response());
};
const httpLink = createHttpLink({
uri: graphql_url,
fetch: customFetch,
});
Another way to prevent outbound requests is to just omit the link property:
return new ApolloClient({
link: user.isLoggedIn
? authorizationHeaderLink.concat(httpLink)
: undefined,
cache,
});
}
That looks way cleaner but now the problem is that make queries that can't be fulfilled by the cache to hang on loading forever.(related issue)
I'm looking for a cleaner and safer way to do this.

React-testing-library with connected react router together

I am trying to test a workflow with a React app. When all fields are filled withing a workflow step, user is able to click to the "next" button. This action registers a state in a reducer and changes the URL to go to the next workflow step.
According to the RTL documentation, I wrap my component under test in a store provider and a connected router using this function:
export const renderWithRedux = (ui: JSX.Element, initialState: any = {}, route: string = "/") => {
// #ts-ignore
const root = reducer({}, { type: "##INIT" })
const state = mergeDeepRight(root, initialState)
const store = createStoreWithMiddleWare(reducer, state)
const history = createMemoryHistory({ initialEntries: [route]})
const Wrapper = ({ children }: any) => (
<Provider store={store}>
<ConnectedRouter history={history}>{children}</ConnectedRouter>
</Provider>
)
return {
...render(ui, { wrapper: Wrapper }),
// adding `store` to the returned utilities to allow us
// to reference it in our tests (just try to avoid using
// this to test implementation details).
history,
store
}
}
Unlike in the documentation, I amm using connected-react-router, not react-router-dom, but I've seen some people using connected-react-router with RTL on the web so I don't think the problem come from here.
The component under test is wrapped in a withRouter function, and I refresh the URL via the connected react router push function, dispatching via the redux connectfunction:
export default withRouter(
connect<any, any, any>(mapStateToProps, mapDispatchToProps, mergeProps)(View)
)
Everything work well in production, but the page doesn't refresh when I fire a click event on the "next" button. Here is the code of my test (to make it easier to read for you, I have filled all field and enable the "next" button):
const {
container,
queryByText,
getAllByPlaceholderText,
getByText,
debug,
getAllByText
} = renderWithRedux(<Wrapper />, getInitialState(), "/workflow/EXAC")
await waitForElement(
() => [getByText("supplierColumnHeader"), getByText("nextButton")],
{ container }
)
fireEvent.click(getByText("nextButton"))
await waitForElement(
() => [getByText("INTERNAL PARENT"), getByText("EXTERNAL PARENT")],
{ container }
)
Any clue of what is going wrong here?

React: Apollo Client: Cannot update during an existing state transition

I am trying to make a component that wraps Apollo Client's Query component. I am using apollo-link-state for local state management and I want to have an error notification system that notifies the user of all the things.
my component looks like this...
export class Viewer extends React.Component {
static propTypes = {
children: PropTypes.func
};
render() {
const { children } = this.props;
return (
<Query query={GET_VIEWER}>
{({ data, client, error }) => {
if (error) {
client.mutate({
mutation: ADD_NOTIFICATION,
variables: { message: unpackApolloErr(error), type: 'Error' }
});
}
return children(data.viewer ? data.viewer : user);
}}
</Query>
);
}
}
but when it tries to add the error with the mutation, I get the react error..
Warning: forceUpdate(...): Cannot update during an existing state transition (such as within `render` or another component's constructor). Render methods should be a pure function of props and state; constructor side-effects are an anti-pattern, but can be moved to `componentWillMount`.
I don't see an obvious way around this and I don't see why it is even happening if the client is provided as a render prop and cannot be used...I must be missing something simple but I cant see what it is
The answer turned out to be that the client is available in the onError function as this when using apollo-link-error https://www.apollographql.com/docs/link/links/error.html
I had tried to use onError before to access the client, but it was not obvious that the scope which it is called in does indeed contain the client even if the client is declared after the onError handler
// client is available here even though it is defined first
const err = onError({ graphQLErrors }) => {
this.client.mutate({ mutation: ADD_ERROR, variables: { graphQLErrors }})
}
const client = new ApolloClient({
cache,
link: ApolloLink.from([err, httpLink])
})
The reason why there was a Warning: forceUpdate was probably because Apollo internally will cause a forceUpdate when there is a mutation. So mutations should be in the render method.
The best way to handle mutations after an Apollo error is to add an onError link as described in https://www.apollographql.com/docs/react/features/error-handling.html#network
// Add errorLink into Apollo Client
const errorLink = onError(({ graphQLErrors, networkError, operation }) => {
this.client.mutate({ mutation: ..., variables: {}});
});
const client = new ApolloClient({
cache,
link: ApolloLink.from([err, httpLink])
})

right way to POST data to a server and handle response with redux

I'm very new to react and redux.
Now I want to rewrite my post request with a redux process.
my current request looks like this:
_handleSubmit(event) {
axios
.post('/createUrl', {
url: this.state.url
})
.then((response) => {
this.setState({
shortenInfos: response.data
})
})
.catch((error) => {
console.log(error);
});
event.preventDefault()
}
now I created a store:
export default function url(state = 0, action) {
switch (action.type) {
case 'CREATE_URL':
// maybe axios request?!
return `${action.url}/test`
case 'CREATED_URL':
return `${action.url}/created`
default:
return state
}
}
so where I must use my store.dispatch()? Should I make my _handleSubmit something like this?
_handleSubmit(event) {
axios
.post('/createUrl', {
url: this.state.url
})
.then((response) => {
store.dispatch({
type: 'CREATED_URL',
url: response.data
})
})
.catch((error) => {
console.log(error);
});
event.preventDefault()
}
I think this is wrong? And where I must use mapStateToProps method? Or should I do the axios-request in my CREATE_URL in my reducer?
Introduction
Using React with Redux gives you high freedom on how you can do things. The downside of this is that it can be hard to find out how things should be done properly, mainly because there is no standard or comprehensive guide to the use of the many dependency you need for a properly implemented project. This answer will guide you through the basics with links to references that will help you to find out wheres next and how to deeper your knowledge.
Reducer
Reducers should be pure, meaning that they have no side effects (like making axios requests) and they should always return a new object/array/value instead of changing the previous state. It is also a good practice to use action types as constants. You can place action types wherever you want, but for simplicity I will put them into the reducer's file, but there are better ways to organize them like using ducks.
export const CREATED_URL = 'CREATE_URL';
export default const url = (state = '', action) => {
switch (action.type) {
case CREATED_URL:
return action.url;
default:
return state;
}
};
Asynchronous actions
Everything that causes a side effect should be an action, so XHR should happen there. Because XHR should be asynchronous it is recommended to use a middleware: redux-thunk and redux-saga are two popular solutions. I will go with thunk so install it first.
First (because const has temporal dead zone) you need an action that will "load" the result of the XHR to the store:
import { CREATED_URL } from './reducer';
const createdUrl = url => ({
type: CREATED_URL,
url, // ES6 trailing comma for prettier git diffs
});
Then you can create the action that will fire the XHR, wait for the response then load it to the store using the action created previously. We need to return a function that will receive dispatch as the parameter. This technique is used in functional programming and is called currying.
export const createUrl = url => dispatch => { // with only 1 parameter the parentheses can be omited
axios
.post('/createUrl', { url }) // ES6 Shorthand property name in { url }
.then(response => {
dispatch(createdUrl({
url: response.data,
})
})
.catch(error => {
// #TODO dispatch an action that will show a message
// notifying the user that the request failed
console.log(error);
});
}
Usage in the React component.
Preparation
For ease of use, you need to connect your React component with Redux. react-redux comes to the rescue. Read the API documentation and add the <Provider> component to the root of your React component tree.
Now, in the top of your React component's file, import all the necessary stuff:
import React from 'react';
import { connect } from 'react-redux';
import { bindActionCreators } from 'redux';
import { createUrl } from './reducer';
mapStateToProps and mapDispatchToProps
Then create the two helper functions for connect:
const mapStateToProps = store => ({ url: store.url })
const mapDispatchToProps = dispatch => bindActionCreators({ createUrl }, dispatch)
With the help of mapStateToProps you can subscribe to store updates and inject the important parts of the Redux store to your components props. mapStateToProps should return an object that will be merged to the component's props. Usually we just do something like store => store.url but because our example is so simple that the reducer returns a plain string instead of something more complex in an object, we need to wrap that string into an object over here.
mapDispatchToProps with the help of bindActionCreators will inject the passed actions to the component's props so we can call and pass them down to subcomponents with ease: this.props.createUrl().
The component itself
Now we can create the component itself. I will use an ES6 class to show an example with componentDidMount, but if you don't need that and you have a stateless component, you can go with a function too.
class Example extends React.Component {
componentDidMount() {
// If you need to load data from a remote endpoint place the action call here, like so:
// this.props.createUrl('your-url');
}
render() {
return (
<div>
<div>URL injected from the store, automatically refreshed on change: {this.props.url}</div>
<div onClick={event => {this.props.createUrl('your-url');}}>Click me to fetch URL</div>
</div>
)
}
}
export default connect(mapStateToProps, mapDispatchToProps)(Example)

Resources