I'm trying to link up React Apollo with Redux so Apollo performs the queries and mutations, and the returned data is dispatched to the Redux store in order to distribute the data around the app.
I believe I'm close to getting it right, but for some reason the app goes into an infinite loop of Redux dispatches, and I can't figure out why.
See code below:
class Admin extends Component {
constructor(props) {
super(props);
}
render({
adminAllTokens
}, {}) {
return ( /* JSX */ )
);
}
}
const AllRefreshTokens = gql `
query {
allUsers {
refreshToken
email
}
}
`;
const gqlWrapper = graphql(AllRefreshTokens, {
props: ({
ownProps,
data
}) => {
ownProps.receivedAdminTokens(data.allUsers); //dispatch to Redux store
return {
...data,
gqladminAllTokens
};
}
});
function mapStateToProps(state, ownProps) {
return {
adminAllTokens: state.auth.adminAllTokens
};
}
function mapDispatchToProps(dispatch) {
return {
receivedAdminTokens: tokens => {
dispatch(adminTokensReceived(tokens));
}
};
}
const reduxWrapper = connect(mapStateToProps, mapDispatchToProps);
export default compose(reduxWrapper, gqlWrapper)(Admin);
The adminTokensReceived() action is in the reducer file:
export const adminTokensReceived = tokens => ({
type: 'ADMIN_TOKENS_RECEIVED',
tokens
});
The GraphQL query only sends one network request, but the console is showing the ADMIN_TOKENS_RECEIVED action dispatching constantly and crashes the browser.
Thanks in advance
Whenever the Apollo HOC receives new props, it causes your action to fire, which updates the store and sends new props to your Apollo HOC, which causes your action to fire...
There's a couple of different ways you could handle this. In my mind, the most straightforward would be to drop the graphql HOC and use withApollo instead. Something like:
compose(
withApollo,
connect(mapStateToProps, mapDispatchToProps)
lifecycle({
componentDidMount() {
const { client } = this.props
client.query({ query: AllRefreshTokens })
.then(({data}) => {
receivedAdminTokens(data.allUsers)
})
.catch( //any error handling logic )
}
})
)
The above uses recompose's lifecycle but you could just as easily stick the componentDidMount method inside your component.
That said, it seems a little redundant to use Redux to store the results of your GraphQL queries when Apollo already does it for you.
Apollo's default behavior is to retrieve the data from the cache first, and only make a network request if the data doesn't exist (which is also why you only saw the one network call). That means any number of components inside your app could be wrapped with the same graphql HOC, and only the first component to be rendered would trigger a request to your GraphQL endpoint -- all other components would get their data from the cache.
Related
I managed to write reducer using createSlice but the action seems to be confusing.
My old reducer :
function listPeopleReducer(state = {
getPeople:{}
}, action){
switch (action.type) {
case D.LIST_PEOPLE: {
return {
...state
, getPeople:action.payload
}
}
default:{}
}
return state
}
By using createSlice from the redux toolkit, I migrated the reducer to this,
const listPeopleReducer = createSlice({
initialState:{getPeople:{}},
name:"listPeople",
reducers:{
listPeople(state,action){
return {
...state,
getPeople : action.payload
}
}
}
})
My old action, makes an api call inside it, with the help of a helper function makeApiRequest (which takes in parameters and returns the response of the api),
export function listPeople(config: any) {
return function (dispatch: any) {
makeApiRequest(config)
.then((resp) => {
dispatch({
type : D.LIST_PEOPLE,
payload : resp.data
})
})
.catch((error) => {
dispatch({
type : D.LIST_PEOPLE,
payload : error
})
})
}
}
With reduxtool kit, we could do something like,
const listPeople = listPeopleReducer.actions.listPeople;
But, how will I write my custom action that contains the helper function makeApiRequest ?
i.e The old Action should be migrated to reduxtoolkit type.
It's definitely tricky when migrating, since there are some major conceptual changes that you must eventually wrap your head around. I had to do it a couple of times before it clicked.
First, when you are creating const listPeopleReducer with createSlice(), that is not actually what you are creating. A slice is a higher level object that can generate action creators and action types for you, and allows you to export reducers and actions FROM it.
Here are the changes I would make to your code:
const peopleSlice = createSlice({
initialState:{getPeople:{}},
name:"people",
reducers:{
listPeople(state,action){
// uses immer under the hood so you can
// safely mutate state here
state.getPeople = action.payload
}
},
extraReducers:
// each thunk you create with `createAsyncThunk()` will
// automatically have: pending/fulfilled/rejected action types
// and you can listen for them here
builder =>
builder.addCase(listPeople.pending, (state,action) => {
// e.g. state.isFetching = true
})
builder.addCase(listPeople.fulfilled, (state,action) => {
// e.g. state.isFetching = false
// result will be in action.payload
})
builder.addCase(listPeople.rejected, (state,action) => {
// e.g. state.isFetching = false
// error will be in action.payload
})
}
})
Then, outside of your slice definition, you can create actions by using createAsyncThunk(), and do like:
export const listPeople = createAsyncThunk(
`people/list`,
async (config, thunkAPI) => {
try {
return makeApiRequest(config)
} catch(error) {
return thunkAPI.rejectWithError(error)
// thunkAPI has access to state and includes
// helper functions like this one
}
}
}
The "Modern Redux with Redux Toolkit" page in the Redux Fundamentals docs tutorial shows how to migrate from hand-written Redux logic to Redux Toolkit.
Your makeApiRequest function would likely be used with Redux Toolkit's createAsyncThunk, except that you should return the result and let createAsyncThunk dispatch the right actions instead of dispatching actions yourself.
So, in the docs, we have this code:
https://react-query.tanstack.com/guides/ssr#using-hydration
RE: The use of 'useRef' to store a reference.
// _app.jsx
import { QueryClient, QueryClientProvider } from 'react-query'
import { Hydrate } from 'react-query/hydration'
export default function MyApp({ Component, pageProps }) {
const queryClientRef = React.useRef()
if (!queryClientRef.current) {
queryClientRef.current = new QueryClient()
}
return (
<QueryClientProvider client={queryClientRef.current}>
<Hydrate state={pageProps.dehydratedState}>
<Component {...pageProps} />
</Hydrate>
</QueryClientProvider>
)
}
BUT, I also need to store some fetch calls IN the "cache" in the MyApp.getInitialProps.... how is that gonna happen IF I create an instance with useRef in function above? Meaning, how is my "getInitialProps" gonna get that instance?
MyApp.getInitialProps = async (appContext) => {
// in here, I do a fetch and get some data I need for SSR
// user Session etc...
const { user } = await fetchUserSession();
// WHAT INSTANCE IS THIS?
queryClient.setQueryData('user', user || {});
return {
...appProps,
dehydratedState: dehydrate(queryClient),
}
}
I am currently defining queryClient = new QueryClient() at the top of the page, so "both" can use it. But I think that is causing some issues with hydration when I npm run build this app.
Remember, this is in "_app.js" so I have to use getInitialProps.
The reason I am doing it here is because we need the users session sitewide, no matter what page they and on. So, rather than do this in every single /page/, just do it in _app.js, so the whole site needs that? The /page/ are Static Generated.
for prefetching on the server, you just create a new QueryClient like described further down on the page you have linked:
export async function getStaticProps() {
const queryClient = new QueryClient()
await queryClient.prefetchQuery('posts', getPosts)
return {
props: {
dehydratedState: dehydrate(queryClient),
},
}
}
Here, you create a new empty client, prefetch and take the state and dehydrate it. Then, on the frontend, that state is put into your instance client from MyApp. This is just a way of getting the data from that server-side cache into the client-side cache.
I have been trying to persist my redux store through a reload. I was using useEffect to dispatch my actions at first but then when I tried to reload the page router became undefined and I got a 500 error. After that I tried using getInitialProps and use the ctx.query.id but I ran into another error saying that hooks can only be called inside of the body of a function component.
How do I make it so hooks work inside of getInitialProps and what is the best way of persisting my store data through a reload?
export default function CarPage() {
const dispatch = useDispatch()
const router = useRouter()
const car = useSelector((state) => state.cars.car)
/*
useEffect(() => {
if(!car && router) {
dispatch(getCar(router.query.id))
}
}, [])
*/
return (
<Container>
<h2>{car.model}</h2>
</Container>
)
}
CarPage.getInitialProps = async (ctx) => {
const dispatch = useDispatch()
dispatch(getCar(ctx.query.id))
}
To persist redux store through a page reload, we definitely need to use browser storage.
I suggest using https://github.com/rt2zz/redux-persist.
To use dispatch inside getInitialProps, please try with this code snippet instead of using useDispatch() hook.
CarPage.getInitialProps = async ({ store, query }) => {
store.dispatch(getCar(query.id));
return { initialState: store.getState() };
}
The Redux manual says every reducer should be a pure function and even no API call should be made, I then curious to know, then, when should I get chance to save my App state tree to an external storage or the backend?
You can save your redux store using and action with the Redux Thunk middleware.
Lets say you want to want to save the store when the user clicks save. First, define an action to do the save:
actions/save.js
import fetch from 'isomorphic-fetch'
export const save = state => {
return () => {
fetch('/api/path/to/save', {
body: JSON.stringify(state),
headers: {
'content-type': 'application/json'
}
method: 'POST'
}
}
}
Then in your component:
components/SaveButton.js
import React from 'react'
import { connect } from 'react-redux';
import { save } from '../actions/save'
const SaveButton = props => {
let { onSave, state } = props
return <button onClick={onSave(state)}>Save</button>
}
const mapStateToProps = state => {
return {state}
}
const mapDispatchToProps = dispatch => {
return {
onSave: state => dispatch(save(state))
}
}
export default connect(mapStateToProps, mapDispatchToProps)(SaveButton)
You shouldn't do that as part of your reducer.
Instead, whenever you want to save some part of your state, you should dispatch an asynchronous action (with the help of middleware like redux-thunk) perhaps called SAVE_XYZ with it's payload being the part of the store you want to save.
dispatch(saveXYZ(data))
saveXYZ needs to be an async action creator that will dispatch the API call to persist your data, and handle the response accordingly.
const saveXYZ = payload => dispatch => {
dispatch(saveXYZPending());
return apiCallToStore(...)
.then(data => saveXYZDone())
.catch(err => saveXYZError());
}
You can read more on async actions and how to handle them.
Two basic approaches:
Use store.subscribe(callback), and write a callback that gets the latest state and persists it after some action has been dispatched
Write a middleware that persists the state when some condition is met
There's dozens of existing Redux store persistence libraries available that will do this work for you.
I'm using the omniauth-github strategy and upon a button click I want to dispatch an action to another domain, (such as 'https://github.com/login/oauth/authorize'). When using dispatch this however does not work as the browser preflights my request and resonds with 'No 'Access-Control-Allow-Origin'. I can get this to work by using an and point to the url, which then will send the user back to my backend to authenticate the user get the token store it. But without dispatch, I have to send back the JWT token my site generates in query params, and since I am omitting my action creators and reducers, I cannot store it in localStorage. Is there any way to perform dispatch cross domain?
export const loginGitHub = () => {
return dispatch => {
fetch('https://github.com/login/oauth/authorize?client_id=...&scope=user',{
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
},
mode: 'cors'
})
.then(resp=>resp.json())
.then(data => {
debugger
})
}
}
You'll need to provide your redux store's dispatch method to this method for it to work, this is typically done by using mapDispatchToProps with redux's connect() method: https://github.com/reduxjs/react-redux/blob/master/docs/api.md
That's the typical flow, if for some reason you need to call this outside a component like before you mount your React app (but after you've initialized your redux store) something like this can work:
import { createStore } from 'redux'
const store = createStore();
export const loginGitHub = dispatch => {
return dispatch => {
fetch('https://github.com/login/oauth/authorize?client_id=...&scope=user',{
method: 'GET',
headers: {
'Access-Control-Allow-Origin': '*',
},
mode: 'cors'
})
.then(resp=>resp.json())
.then(data => {
debugger
})
}
}
loginGitHub(store.dispatch);
That's very much an anti pattern, and I'd recommend properly using mapDispatchToProps which requires
Creating a store
Wrapping your app in a provider and providing the previously created store as a prop to the provider.
Using connect() like so within your component:
import React, { Component } from 'react';
import { connect } from 'redux';
import { loginGitHub } from './wherever';
class ExampleComponent extends Component {
// whatever component methods you need
}
const mapDispatchToProps = dispatch => ({
loginGitHub: () => dispatch(logInGitHub())
})
export default connect(null, mapDispatchToProps)(ExampleComponent);
Then you'll be able to call loginGitHub with this.props.loginGitHub() within your component.