Accessing useAuth0 hook data via redux thunk action with axios instance - react-redux

Have a bit of an issue attempting to get Auth0 info on the logged-in user with our current architecture.
We have redux with #reduxjs/toolkit & react-redux as our state management tool.
We use axios to make HTTP requests via redux-thunk actions.
And now we have a part of our application that allows users to signup/login with Auth0.
So, an example of our problem.
Currently our redux store is setup with some reducers
/* eslint-disable import/no-cycle */
import { configureStore } from '#reduxjs/toolkit';
import thunk from 'redux-thunk';
const createStore = (initialState?: any) => {
return configureStore({
reducer: {
// reducers are here
},
middleware: [thunk],
preloadedState: initialState,
});
};
export default createStore;
Then we attached that to a Provider at the base of our application
import React from 'react';
import { Provider } from 'react-redux';
import createStore from '../store/createStore';
const App = () => {
return (
<Provider store={createStore()}>
//
</Provider>
);
};
export default App;
We have an axios instance function that uses axios to make HTTP requests and handles errors.
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import { getAuthSignature } from '../utils/auth';
export const API_URL = process.env.API_HOST;
const axiosInstance = async <T = any>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
const { token } = await getAuthSignature();
// I need to access auth0 data here
const { getAccessTokenSilently, isAuthenticated, isLoading, loginWithRedirect, user } = auth0;
if (!token) {
const tokenErr = {
title: 'Error',
message: 'Missing Authentication Token',
success: false,
};
throw tokenErr;
}
try {
let accessToken = token;
// Update authorization token if auth0 user
if(auth0) {
if(isAuthenticcation && user) accessToken = await getAccessTokenSilently({ audience });
else loginWithRedirect();
}
const result = await axios({
...requestConfig,
headers: {
...requestConfig.headers,
authorization: `Bearer ${accessToken}`,
},
});
return result;
} catch (error: any) {
if (error.response) {
if ([401, 403].includes(error.response.status)) {
window.location = '/';
}
const contentType = error?.response?.headers?.['content-type'];
const isHTMLRes = contentType && contentType.indexOf('text/html') !== -1;
const errObj = {
status: error?.response?.status,
statusText: error?.response?.statusText,
errorMessage: isHTMLRes && error?.response?.text && (await error?.response?.text()),
error,
};
throw errObj;
}
throw error;
}
};
export default axiosInstance;
This in an example of a thunk action, we would have something like this that uses the axios instance mentioned above to make the HTTP requests.
import axios, { API_URL } from '../../services/axios';
import { Result } from '../../types/test';
import { AppThunk } from '../../store/store';
import { setResults, setResultsLoading, setTableLoading } from './test.slice';
type DefaultThunk = () => AppThunk<Promise<void>>;
const getResults: DefaultThunk = () => async () => {
dispatch(setTableLoading(true));
try {
const result = await axios<Result[]>(
{
method: 'GET',
url: `${API_URL}/test`,
},
);
dispatch(setResults(result.data));
} catch (err: any) {
console.log({ err });
} finally {
dispatch(setResultsLoading(false));
dispatch(setTableLoading(false));
}
};
export default getResults;
We then dispatch our thunk actions to make HTTP requests and update reducer states in our React components.
import React, { useEffect } from 'react';
import { useSelector, useDispatch } from 'react-redux';
import getResults from '../../reducers/test/test.thunk';
const TestComponent = () => {
const dispatch = useDispatch();
useEffect(() => {
dispatch(getResults());
}, []);
return (
//
);
};
export default TestComponent;
My problem is that I have no idea how to integrate Auth0 gracefully into the current flow, so I do not have to make checks in every react component that uses a thunk action.
Basically I need access to values within the useAuth0 hook from #auth0/auth0-react for example getAccessTokenSilently, isAuthenticated, user & loginWithRedirect. Just to name a few.
We can't use the useAuth0 hook in the axios instance file, as it's not a react component/hook, nor is the thunk file.
So I'm not sure how and where the best place is to get the data so that it is accessible in the axios file, as aforementioned without having to pass it as an argument or something in every redux thunk action.
Perhaps we just need a different approach to the current flow of dispatch > action > axios request?
Is there any way to pass this data in as middleware to redux?
Any help would be greatly appreciated.

I don't believe you'd be able to use a middleware to "sniff" out the auth0 context value because middlewares run outside React. What I'd suggest here is to create a wrapper component that sits between the Auth0Provider and redux Provider components that accesses the auth0 context and dispatches an action to save it into the redux state where it can be selected via useSelector or accessed directly from store.getState().
Fortunately it appears the auth0 context value is already memoized here so it should be able to be directly consumed as a stable reference within the app.
Rough Example:
import { useDispatch } from 'react-redux';
import { useAuth0 } from '#auth0/auth0-react';
import { actions } from '../path/to/auth0Slice';
const Auth0Wrapper = ({ children }) => {
const dispatch = useDispatch();
const auth0 = useAuth0();
useEffect(() => {
dispatch(actions.setAuthContext(auth0));
}, [auth0]);
return children;
};
Create and export the store for consumption within the app.
Store
import { configureStore } from '#reduxjs/toolkit';
import thunk from 'redux-thunk';
import { combineReducers } from 'redux';
...
import auth0Reducer from '../path/to/auth0Slice';
...
const rootReducer = combineReducers({
auth0: auth0Reducer,
... other root state reducers ...
});
const createStore = (initialState?: any) => {
return configureStore({
reducer: rootReducer,
middleware: [thunk],
preloadedState: initialState,
});
};
export default createStore;
App
import Auth0Wrapper from '../path/to/Auth0Wrapper';
import createStore from '../path/to/store';
const store = createStore();
const App = () => {
return (
<Auth0Provider ......>
<Provider store={store}>
<Auth0Wrapper>
// ... JSX ...
</Auth0Wrapper>
</Provider>
</Auth0Provider>
);
};
export store;
export default App;
Create a new Auth0 state slice.
import { createSlice } from '#reduxjs/toolkit';
const auth0Slice = createSlice({
name: 'auth0',
initialState: {},
reducers: {
setAuthContext: (state, action) => {
return action.payload;
},
},
});
export const actions = {
...auth0Slice.actions,
};
export default auth0Slice.reducer;
From here you can import the exported store object and access the current state inside the axios setup.
import axios, { AxiosRequestConfig, AxiosResponse } from 'axios';
import store from '../path/to/App';
import { getAuthSignature } from '../utils/auth';
export const API_URL = process.env.API_HOST;
const axiosInstance = async <T = any>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
const { token } = await getAuthSignature();
const { auth0 } = store.getState(); // <-- access current state from store
const {
getAccessTokenSilently,
isAuthenticated,
isLoading,
loginWithRedirect,
user
} = auth0;
...
};

The hook methods are great if you're not using redux, but since you are, the recommended approach is to use the spa js library - https://github.com/auth0/auth0-spa-js/.
Here's a code example for a rest call:
document.getElementById('call-api').addEventListener('click', async () => {
const accessToken = await auth0.getTokenSilently();
const result = await fetch('https://myapi.com', {
method: 'GET',
headers: {
Authorization: `Bearer ${accessToken}`
}
});
const data = await result.json();
console.log(data);
});
https://github.com/auth0/auth0-spa-js/blob/master/EXAMPLES.md#calling-an-api
This is easily adaptable to thunks, in your case, inside of your axios instance ie:
const axiosInstance = async <T = any>(requestConfig: AxiosRequestConfig): Promise<AxiosResponse<T>> => {
const accessToken = await auth0.getTokenSilently();
// handle token and request
}
The auth0 with hooks is more like a convenience library, but it's built on top of spa js.

Related

Apollo GraphQL react client: subscription

I was trying to make a little demo with GraphQL subscriptions and GraphQL Apollo client.
I already have my GraphQL API, but when I try to use Apollo client, it looks like it doesn't complete the websocket subscribe step:
import React from 'react';
import ReactDOM from 'react-dom/client';
import './index.css';
import { ApolloClient, InMemoryCache, ApolloProvider, gql, useQuery } from '#apollo/client';
import { split, HttpLink } from '#apollo/client';
import { getMainDefinition } from '#apollo/client/utilities';
import { GraphQLWsLink } from '#apollo/client/link/subscriptions';
import { createClient } from 'graphql-ws';
import { useSubscription } from '#apollo/react-hooks'
import reportWebVitals from './reportWebVitals';
const httpLink = new HttpLink({
uri: 'https://mygraphql.api'
});
const wsLink = new GraphQLWsLink(createClient({
url: 'wss://mygraphql.api',
options: {
reconnect: true
}
}));
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query);
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
);
},
wsLink,
httpLink,
);
const client = new ApolloClient({
link: splitLink,
cache: new InMemoryCache(),
fetchOptions: {
mode: 'no-cors',
}
});
const FAMILIES_SUBSCRIPTION = gql`
subscription{
onFamilyCreated {
id
name
}
}
`;
function LastFamily() {
const { loading, error, data } = useSubscription(FAMILIES_SUBSCRIPTION, {
variables: { },
onData: data => console.log('new data', data)
});
if (loading) return <div>Loading...</div>;
if (error) return <div>Error!</div>;
console.log(data);
const family = data.onFamilyCreated[0];
return (
<div>
<h1>{family.name}</h1>
</div>
);
}
const root = ReactDOM.createRoot(document.getElementById('root'));
root.render(
(<ApolloProvider client={client}>
<div>
<LastFamily />
</div>
</ApolloProvider>));
reportWebVitals();
According to graphql-transport-ws, to accomplish a success call, it should call connection_init and subscribe message. But when I open Dev Tools, it only sends "connection_init"
I'm expecting this output:
What step should I add to accomplish a successful call using graphql-transport-ws?
P.s. I'm not a React Developer, just be kind.
The solutions I'm putting up are based on #apollo/server v.4, with expressMiddleware and mongodb/mongoose on the backend and subscribeToMore with updateQuery on the client-side instead of useSubscription hook. In light of my observations, I believe there may be some issues with your backend code that require refactoring. The transport library graphql-transport-ws has been deprecated and advise to use graphql-ws. The following setup also applies as of 12.2022.
Subscription on the backend
Install the following dependencies.
$ npm i #apollo/server #graphql-tools/schema graphql-subscriptions graphql-ws ws cors body-parser mongoose graphql express
Set up the db models, I will refer to mongodb using mongoose and it might look like this one e.g.
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const model = mongoose.model
const FamilySchema = new Schema({
name: {
type: String,
unique: true, //optional
trim: true,
}
})
FamilySchema.virtual('id').get(function () {
return this._id.toHexString()
})
FamilySchema.set('toJSON', {
virtuals: true,
transform: (document, retObj) => {
delete retObj.__v
},
})
const FamilyModel = model('FamilyModel', FamilySchema)
export default FamilyModel
Setup schema types & resolvers; it might look like this one e.g.
// typeDefs.js
const typeDefs = `#graphql
type Family {
id: ID!
name: String!
}
type Query {
families: [Family]!
family(familyId: ID!): Family!
}
type Mutation {
createFamily(name: String): Family
}
type Subscription {
familyCreated: Family
}
`
// resolvers.js
import { PubSub } from 'graphql-subscriptions'
import mongoose from 'mongoose'
import { GraphQLError } from 'graphql'
import FamilyModel from '../models/Family.js'
const pubsub = new PubSub()
const Family = FamilyModel
const resolvers = {
Query: {
families: async () => {
try {
const families = await Family.find({})
return families
} catch (error) {
console.error(error.message)
}
},
family: async (parent, args) => {
const family = await Family.findById(args.familyId)
return family
},
Mutation: {
createFamily: async (_, args) => {
const family = new Family({ ...args })
try {
const savedFamily = await family.save()
const createdFamily = {
id: savedFamily.id,
name: savedFamily.name
}
// resolvers for backend family subscription with object iterator FAMILY_ADDED
pubsub.publish('FAMILY_CREATED', { familyCreated: createdFamily })
return family
} catch (error) {
console.error(error.message)
}
}
},
Subscription: {
familyCreated: {
subscribe: () => pubsub.asyncIterator('FAMILY_CREATED'),
}
},
Family: {
id: async (parent, args, contextValue, info) => {
return parent.id
},
name: async (parent) => {
return parent.name
}
}
}
export default resolvers
At the main entry server file (e.g. index.js) the code might look like this one e.g.
import dotenv from 'dotenv'
import { ApolloServer } from '#apollo/server'
import { expressMiddleware } from '#apollo/server/express4'
import { ApolloServerPluginDrainHttpServer } from '#apollo/server/plugin/drainHttpServer'
import { makeExecutableSchema } from '#graphql-tools/schema'
import { WebSocketServer } from 'ws'
import { useServer } from 'graphql-ws/lib/use/ws'
import express from 'express'
import http from 'http'
import cors from 'cors'
import bodyParser from 'body-parser'
import typeDefs from './schema/tpeDefs.js'
import resolvers from './schema/resolvers.js'
import mongoose from 'mongoose'
dotenv.config()
...
mongoose.set('strictQuery', false)
let db_uri
if (process.env.NODE_ENV === 'development') {
db_uri = process.env.MONGO_DEV
}
mongoose.connect(db_uri).then(
() => {
console.log('Database connected')
},
(err) => {
console.log(err)
}
)
const startGraphQLServer = async () => {
const app = express()
const httpServer = http.createServer(app)
const schema = makeExecutableSchema({ typeDefs, resolvers })
const wsServer = new WebSocketServer({
server: httpServer,
path: '/',
})
const serverCleanup = useServer({ schema }, wsServer)
const server = new ApolloServer({
schema,
plugins: [
ApolloServerPluginDrainHttpServer({ httpServer }),
{
async serverWillStart() {
return {
async drainServer() {
await serverCleanup.dispose()
},
}
},
},
],
})
await server.start()
app.use(
'/',
cors(),
bodyParser.json(),
expressMiddleware(server)
)
const PORT = 4000
httpServer.listen(PORT, () =>
console.log(`Server is now running on http://localhost:${PORT}`)
)
}
startGraphQLServer()
Subscription on the CRA frontend
Install the following dependencies.
$ npm i #apollo/client graphql graphql-ws
General connection setup e.g.
// src/client.js
import { ApolloClient, HttpLink, InMemoryCache, split } from '#apollo/client'
import { getMainDefinition } from '#apollo/client/utilities'
import { defaultOptions } from './graphql/defaultOptions'
import { GraphQLWsLink } from '#apollo/client/link/subscriptions'
import { createClient } from 'graphql-ws'
...
const baseUri = process.env.REACT_APP_BASE_URI // for the client
const wsBaseUri = process.env.REACT_APP_WS_BASE_URI // for the backend as websocket
const httpLink = new HttpLink({
uri: baseUri,
})
const wsLink = new GraphQLWsLink(
createClient({
url: wsBaseUri
})
)
const splitLink = split(
({ query }) => {
const definition = getMainDefinition(query)
return (
definition.kind === 'OperationDefinition' &&
definition.operation === 'subscription'
)
},
wsLink,
httpLink
)
const client = new ApolloClient({
cache: new InMemoryCache(),
link: splitLink,
})
export default client
// src/index.js
import React from 'react'
import ReactDOM from 'react-dom/client'
import client from './client'
import { ApolloProvider } from '#apollo/client'
import App from './App'
const root = ReactDOM.createRoot(document.getElementById('root'))
root.render(
<React.StrictMode>
<ApolloProvider client={client}>
<App />
</ApolloProvider>
</React.StrictMode>
)
Define the operations types for the client: queries, mutation & subscription e.g.
// src/graphql.js
import { gql } from '#apollo/client'
// Queries
export const FAMILIES = gql`
query Families {
families {
id
name
}
}
`
export const FAMILY = gql`
query Family($familyId: ID) {
family {
id
name
}
}
`
// Mutation
export const CREATE_FAMILY = gql`
mutation createFamily($name: String!) {
createFamily(name: $name) {
id
name
}
}
`
// Subscription
export const FAMILY_SUBSCRIPTION = gql`
subscription {
familyCreated {
id
name
}
}
Components, it might look like this one e.g.
Apollo's useQuery hook provides us with access to a function called subscribeToMore. This function can be destructured and used to act on new data that comes in via subscription. This has the result of rendering our app real-time.
The subscribeToMore function utilizes a single object as an argument. This object requires configuration to listen for and respond to subscriptions.
At the very least, we must pass a subscription document to the document key in this object. This is a GraphQL document in which we define our subscription.
We can a updateQuery field that can be used to update the cache, similar to how we would do in a mutation.
// src/components/CreateFamilyForm.js
import { useMutation } from '#apollo/client'
import { CREATE_FAMILY, FAMILIES } from '../graphql'
...
const [createFamily, { error, loading, data }] = useMutation(CREATE_FAMILY, {
refetchQueries: [{ query: FAMILIES }], // be sure to refetchQueries after mutation
})
...
// src/components/FamilyList.js
import React, { useEffect, useState } from 'react'
import { useQuery } from '#apollo/client'
import { Families, FAMILY_SUBSCRIPTION } from '../graphql'
const { cloneDeep, orderBy } = pkg
...
export const FamilyList = () => {
const [families, setFamilies] = useState([])
const { loading, error, data, subscribeToMore } = useQuery(Families)
...
useEffect(() => {
if (data?.families) {
setFamilies(cloneDeep(data?.families)) // if you're using lodash but it can be also setFamilies(data?.families)
}
}, [data?.families])
useEffect(() => {
subscribeToMore({
document: FAMILY_SUBSCRIPTION,
updateQuery: (prev, { subscriptionData }) => {
if (!subscriptionData.data) return prev
const newFamily = subscriptionData.data.familyCreated
if (!prev.families.find((family) => family.id === newFamily.id)) {
return Object.assign({}, prev.families, {
families: [...prev.families, newFamily],
})
} else {
return prev
}
},
})
}, [subscribeToMore])
const sorted = orderBy(families, ['names'], ['desc']) // optional; order/sort the list
...
console.log(sorted)
// map the sorted on the return statement
return(...)
END. Hard-coding some of the default resolvers are useful for ensuring that the value that you expect will returned while avoiding the return of null values. Perhaps not in every case, but for fields that refer to other models or schema.
Happy coding!

How to display fetched data in functional react-redux component?

Why I see always "loading..."?
I used redux-toolkit and createSlice and fetch data by axios.
I have not any problem by fetching data and my data is in State.
My problem is displaying fetched data.
My Component code is:
import React, {useEffect, useState} from 'react';
import { useDispatch, useSelector } from 'react-redux';
import {fetchTrackers} from 'dashboard/dashboardSlice';
export default function TrackerManagerDashboard() {
const [trackersList, setTrackersList] = useState(useSelector(state => state.trackersData));
const [activeTracker, setActiveTracker] = useState(useSelector(state => state.activeTracker));
const dispatch = useDispatch();
useEffect(() => {
dispatch(fetchTrackers);
}, []);
if(!trackersList)
return (
<div>loading...</div>
)
return (
<div className="TrackerManagerDashboard">
...
</div>
)
}
and reducer Slice file is:
import { createSlice } from '#reduxjs/toolkit'
import * as env from "../../environments";
import axios from 'axios';
const initialState = {
trackersData: {},
activeTracker: {},
}
const dashboardSlice = createSlice({
name: 'dashboard',
initialState,
reducers: {
setInitialState(state, action) {
state.trackersData = action.payload.data;
state.activeTracker = state.trackersData[Object.keys(state.trackersData)[0]];
},
},
})
export async function fetchTrackers (dispatch, getState) {
try {
const { data } = await axios.get(env.APP_URL + '/fetch/trackers.json');
dispatch(setInitialState({type: 'setInitialState', data }));
} catch(error) {
console.log(error);
}
}
export const { setInitialState} = dashboardSlice.actions
export default dashboardSlice.reducer
Never use useState(useSelector.
That means "create a component-local state with the initial value that the return value of useSelector at the time of first render has". If the Redux state changes later, you will never get to see it, as your useState is already initialized and any change there will not be reflected in your trackersList variable.
Instead, just call useSelector:
const trackersList = useSelector(state => state.trackersData);
const setActiveTracker = useSelector(state => state.activeTracker);

[updated]Intergrating NextJS and Redux State Management

this my updated version of intergrating redux and NextJS. Just to elobarate what I have done so far...
STEP 1. I've created a store.js file to set up my global store in reference to github's explanation from nextJS developers.
import { createStore, applyMiddleware, combineReducers } from 'redux';
import { createWrapper, HYDRATE } from 'next-redux-wrapper';
import thunkMiddleware from 'redux-thunk';
import { customerListReducer } from './customerReducers';
const bindMiddleware = (middleware) => {
if (process.env.NODE_ENV !== 'production') {
const { composeWithDevTools } = require('redux-devtools-extension');
return composeWithDevTools(applyMiddleware(...middleware));
}
return applyMiddleware(...middleware);
};
const combinedReducer = combineReducers({
customerList: customerListReducer,
});
const reducer = (state, action) => {
console.log('Just Displaying the Store', state);
if (action.type === HYDRATE) {
const nextState = {
...state, // use previous state
...action.payload, // apply delta from hydration
};
if (state.count) nextState.count = state.count; // preserve count value on client side navigation
return nextState;
} else {
return combinedReducer(state, action);
}
};
// create a makeStore function
const store = () =>
createStore(
reducer,
bindMiddleware([thunkMiddleware])
);
// export an assembled wrapper
export const wrapper = createWrapper(store);
STEP 2: Imported the wrapper above in my _app file to make the wrapper available across all pages in my application
import Nav from '../components/Nav';
import {wrapper} from '../reducers/store';
function MyApp({ Component, pageProps }) {
return (
<>
<Nav />
<Component {...pageProps} />
</>
);
}
export default wrapper.withRedux(MyApp);
STEP 3: CONFIGURATIONS
A) My Action that calls external API
import axios from 'axios';
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
export const listCustomers = () => async (dispatch) => {
try {
dispatch({
type: CUSTOMER_LIST_REQUEST,
});
const { data } = await axios.get(
'https://byronochara.tech/gassystem/api/v1/customers'
);
const result = data.results;
dispatch({
type: CUSTOMER_LIST_SUCCESS,
payload: result,
});
} catch (error) {
dispatch({
type: CUSTOMER_LIST_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
B)My Action Reducer
import {
CUSTOMER_LIST_REQUEST,
CUSTOMER_LIST_SUCCESS,
CUSTOMER_LIST_FAIL,
} from '../constants/customerConstants';
import { HYDRATE } from 'next-redux-wrapper';
export const customerListReducer = (state = { customers: [] }, action) => {
switch (action.type) {
case HYDRATE:
return { loading: true, customers: [] };
case CUSTOMER_LIST_REQUEST:
return { loading: true, customers: [] };
case CUSTOMER_LIST_SUCCESS:
return {
loading: false,
customers: action.payload,
};
case CUSTOMER_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
C)The finally bringing it all together in my index.js page to display the results:
import React, { useEffect } from 'react';
import Head from 'next/head';
import { useSelector} from 'react-redux';
import { listCustomers } from './../actions/customerActions';
import { wrapper } from '../reducers/store';
import styles from '../styles/Home.module.css';
const Home = () => {
//Select the loaded customers' list from central state
const customerList = useSelector((state) => {
console.log(state);
return state.customerList;
});
const { loading, error, customers } = customerList;
//displaying the customers data from the external API
console.log('Fetched Customers Data', customers);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{/* {loading && <h6>Loading...</h6>} */}
{/* {error && <h6>Error Occured...</h6>} */}
{/* {customers.map((customer) => (
<h3>{customer.customerName}</h3>
))} */}
{/* <ArticleList customers={customers} /> */}
</div>
);
};
// getStaticProp at build time
// getServerSideProp at every request slower
// getStaticPath to dynamically generate paths based on the data we are fetching
export const getStaticProps = wrapper.getServerSideProps(async ({ store }) => {
// console.log('STORE', store);
store.dispatch(listCustomers());
});
export default Home;
COMMENT ON THE PROBLEM I'M FACING FROM THE ABOVE CODE: once everything has been set up if you follow the code above, the code seems to run well the store is successfully created when I log the result on the console ``{ customerList: { loading: true, customers: [] } }. But then I guess this is the result from the HYDRATE action type since it will always be dispatch since am using getStaticProps``` that creates a new store instance in the server.
MAIN QUIZ: My challenge is how do I bypass the HYDRATED action and reconcile the server side state with the client side store and persist it and at least to finally be able to view the list from the external API. Thanks in advance. :)
I totally recommend you to use reduxjs/toolkit. It's very simple , less code, no wrappers, clean. And no matter your project on nextjs or created via CRA. Also you dont need to configure redux-thunk and redux-devtools cause they are enabled by default. Read documentation for more information ( how to persist state without any npm package and so on )
Here is a little example.
store.js
import { combineReducers, configureStore } from "#reduxjs/toolkit";
import userSlice from './user.slice.js';
//reducers
const rootReducer = combineReducers({
user: userSlice
});
const store = configureStore({
reducer: rootReducer,
});
export default store;
Wrap with Provider (in your case _app.js)
<Provider store={store}>
<Component {...pageProps} />
</Provider>
user.slice.js ( action + reducer )
import { createSlice, createAsyncThunk } from '#reduxjs/toolkit';
const initialState = {
id: '',
email: '',
roles: []
};
// export async action
export const signIn = createAsyncThunk('user/signIn', async (data) => {
try {
const payload = await api.auth.signin(data).then((res) => res.data);
// do some stuff if you want
return payload ;
} catch (err) {
console.log(err.response);
}
});
const userSlice = createSlice({
name: 'user',
initialState,
reducers: {
removeUser(state, payload) {
//cant be an async method
return initialState;
},
extraReducers: (builder) => {
builder.addCase(signIn.fulfilled, (state, { payload }) => {
// payload from the async method above (asyncThunk)
return payload;
});
},
},
});
// export actions
export const { removeUser } = userSlice.actions;
// export reducer
export default userSlice.reducer;
Thats it. Last step to call actions from any component e.g.
import { useDispatch, useSelector } from 'react-redux';
import { signIn, removeUser } from '../actions/userSlice';
// in function component
// call hooks
const dispatch = useDispatch();
// read the store
const { user } = useSelector((state) => state);
// dispatch any action , example below
dispatch(signIn(userCredentials));
// or
dispatch(removeUser());
I has an Issue with setting Redux with NextJS and this is my final answer after some insight from mirik999 too.
A. my store.
import { configureStore } from '#reduxjs/toolkit';
//importing the slice file with sliced reducers
import customerListReducer from '../slices/customerSlice';
// const composedEnhancer = composeWithDevTools(applyMiddleware(thunkMiddleware));
const store = configureStore({
reducer: {
customerList: customerListReducer,
},
});
export default store;
B. The store is provided in my app component
function MyApp({ Component, pageProps }) {
return (
<Provider store={store}>
<Nav />
<Component {...pageProps} />
</Provider>
);
}
export default MyApp;
C. The Slice file that automatically creates action creators and the reducer
import { createSlice } from '#reduxjs/toolkit';
//creating and action that calls API from a REST API backend
export const customersFetchedList = createAsyncThunk(
'customersList/customersListSuccess',
async () => {
try {
const { data } = await axios.get(
'https://example.com/api/your/endpoint'
);
const result = data.results;
//the payload
const payload = result;
return payload;
} catch (error) {
console.log(error.response);
const payload =
error.response && error.response.data.message
? error.response.data.message
: error.message;
return payload;
}
}
);
const initialState = {
loading: true,
customers: [],
error: false,
};
const customerListSlice = createSlice({
name: 'customersList',
initialState,
reducers: {
//reducer functions we've provided
customersRequest(state, action) {
if (state.loading == true) {
return state;
}
},
},
extraReducers: (builder) => {
initialState,
builder.addCase(customersFetchedList.fulfilled, (state, action) => {
state.loading = false;
state.customers = action.payload;
state.error = false;
return state;
});
},
});
export const {
customersRequest,
customersLoadingError,
} = customerListSlice.actions;
export default customerListSlice.reducer;
D. Then finally fired this action above in my component using the useEffect()
import React, { useEffect } from 'react';
import Head from 'next/head';
const Home = () => {
//method to fire the action
const dispatch = useDispatch();
//Select the loaded customers' list from central state
const customerList = useSelector((state) => state);
// const { loading, error, customers } = customerList;
useEffect(() => {
dispatch(listCustomers());
}, []);
return (
<div className={styles.container}>
<Head>
<title>Home | Next</title>
</Head>
<h1>Welcome to Home Page</h1>
{loading && <h6>Loading...</h6>}
{error && <h6>Error Occured...</h6>}
{customers.map((customer) => (
<h3>{customer.customerName}</h3>
))}
</div>
);
};
Thanks so much for your contribution. :)

What is the recommended way to pass Apollo Client around in a React app?

Right now I am using the HOC withApollo like:
export default connect(mapStateToProps, mapDispatchToProps)(withApollo(withData(Browse)));
then in that component:
render() {
const { client } = this.props;
<Button onPress={() => searchInterestsTab(client)} />
then outside that component:
export const searchInterestsTab = (client) => {
^ but am finding this gets very messy having to pass it into every outside function from my component.
Couldn't I just use:
const apolloClient = new ApolloClient({...})
export default apolloClient;
then:
import apolloClient from './apolloClient';
everywhere?
It should be possible to use it with kind of:
import apolloClient from './apolloClient'
If you look at the usage documantation you see that you can use it. So somewhere most possible in your index.js you should already have
const apolloClient = new ApolloClient({...})
My apollo client is instantiated like this:
import ApolloClient, { addTypename } from 'apollo-client';
const createApolloClient = options => {
return new ApolloClient(Object.assign({}, {
queryTransformer: addTypename,
dataIdFromObject: (result) => {
if (result.id && result.__typename) {
return result.__typename + result.id;
}
return null;
},
}, options))
};
export default createApolloClient;
and in the index.js it is used like this:
...
const client = createApolloClient({
networkInterface: networkInterface,
initialState: window.__APOLLO_STATE__,
ssrForceFetchDelay: 100,
});
....
export {
client,
...
};

How to make AJAX request in redux

For all I know, I have to write request in action create. How to use a promise in action for submitting a request? I am getting data in action. Then new state is created in reducer. Bind action and reducer in connect. But I don't know how to use promise for request.
Action
import $ from 'jquery';
export const GET_BOOK = 'GET_BOOK';
export default function getBook() {
return {
type: GET_BOOK,
data: $.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return data;
})
};
}
Reducer
import {GET_BOOK} from '../actions/books';
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
return state;
default:
return state;
}
};
export default booksReducer;
Container
How display data in container?
import React, { Component, PropTypes } from 'react';
import { connect } from 'react-redux';
import getBook from '../actions/books';
import Radium from 'radium';
import {Link} from 'react-router';
function mapStateToProps(state) {
return {
books: state.data.books,
};
}
function mapDispatchToProps(dispatch) {
return {
getBooks: () => dispatch(getBook()),
};
}
#Radium
#connect(mapStateToProps, mapDispatchToProps)
class booksPage extends Component {
static propTypes = {
getBooks: PropTypes.func.isRequired,
books: PropTypes.array.isRequired,
};
render() {
const {books} = this.props;
return (
<div>
<Link to={`/authors`}><MUIButton style="flat">All Authors</MUIButton></Link>
<ul>
{books.map((book, index) =>
<li key={index}>
<Link to={`/book/${book.name}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
"{book.name}"</div></MUIButton></Link>
<Link to={`/author/${book.author}`}><MUIButton style="flat"><div class="mui--text-black mui--text-display4">
{book.author}</div></MUIButton></Link>
</li>
)}
</ul>
</div>
);
}
}
export default booksPage;
Since you are already using redux you can apply redux-thunk middleware which allows you to define async actions.
Installation & usage: Redux-thunk
export function fetchBook(id) {
return dispatch => {
dispatch(setLoadingBookState()); // Show a loading spinner
fetch(`/book/${id}`, (response) => {
dispatch(doneFetchingBook()); // Hide loading spinner
if(response.status == 200){
dispatch(setBook(response.json)); // Use a normal function to set the received state
}else {
dispatch(someError)
}
})
}
}
function setBook(data) {
return { type: 'SET_BOOK', data: data };
}
You should use Async Actions described in Redux Documentation
Here an example of reducer for async action.
const booksReducer = (state = {}, action) => {
switch (action.type) {
case 'RESOLVED_GET_BOOK':
return action.data;
default:
return state;
}
};
export default booksReducer;
and then you create your Async Action.
export const getBook() {
return fetch('/api/data')
.then(response => response.json())
.then(json => dispatch(resolvedGetBook(json)))
}
export const resolvedGetBook(data) {
return {
type: 'RESOLVED_GET_BOOK',
data: data
}
}
Several Notes:
We could return Promise (instead of Object) in action by using redux-thunk middleware.
Don't use jQuery ajax library. Use other library specifically for doing that (e.g. fetch()). I use axios http client.
Remember, in redux you only use pure function in reducer. Don't make ajax call inside reducer.
Read the complete guide from redux docs.
You should be able to use dispatch inside the callback (if you pass it as an argument):
export default function getBook(dispatch) {
$.ajax({
method: "GET",
url: "/api/data",
dataType: "json"
}).success(function(data){
return dispatch({type:'GET_BOOK', data: data});
});
}
Then, pass dispatch to the action:
function mapDispatchToProps(dispatch) {
return {
getBooks: () => getBook(dispatch),
};
}
Now, you should have access to the action.data property in the reducer:
const booksReducer = (state = initialState, action) => {
switch (action.type) {
case GET_BOOK:
//action.data <--- here
return state;
default:
return state;
}
};
You might want to separate concerns, to keep action creators "pure".
Solution; write some middleware. Take this for example (using superagent).
import Request from 'superagent';
const successHandler = (store,action,data) => {
const options = action.agent;
const dispatchObject = {};
dispatchObject.type = action.type + '_SUCCESS';
dispatchObject[options.resourceName || 'data'] = data;
store.dispatch(dispatchObject);
};
const errorHandler = (store,action,err) => {
store.dispatch({
type: action.type + '_ERROR',
error: err
});
};
const request = (store,action) => {
const options = action.agent;
const { user } = store.getState().auth;
let method = Request[options.method];
method = method.call(undefined, options.url)
if (user && user.get('token')) {
// This example uses jwt token
method = method.set('Authorization', 'Bearer ' + user.get('token'));
}
method.send(options.params)
.end( (err,response) => {
if (err) {
return errorHandler(store,action,err);
}
successHandler(store,action,response.body);
});
};
export const reduxAgentMiddleware = store => next => action => {
const { agent } = action;
if (agent) {
request(store, action);
}
return next(action);
};
Put all this in a module.
Now, you might have an action creator called 'auth':
export const auth = (username,password) => {
return {
type: 'AUTHENTICATE',
agent: {
url: '/auth',
method: 'post',
resourceName: 'user',
params: {
username,
password
}
}
};
};
The property 'agent' will be picked up by the middleware, which sends the constructed request over the network, then dispatches the incoming result to your store.
Your reducer handles all this, after you define the hooks:
import { Record } from 'immutable';
const initialState = Record({
user: null,
error: null
})();
export default function auth(state = initialState, action) {
switch (action.type) {
case 'AUTHENTICATE':
return state;
case 'AUTHENTICATE_SUCCESS':
return state.merge({ user: action.user, error: null });
case 'AUTHENTICATE_ERROR':
return state.merge({ user: null, error: action.error });
default:
return state;
}
};
Now inject all this into your view logic. I'm using react as an example.
import React from 'react';
import ReactDOM from 'react-dom';
/* Redux + React utils */
import { createStore, applyMiddleware, bindActionCreators } from 'redux';
import { Provider, connect } from 'react-redux';
// thunk is needed for returning functions instead
// of plain objects in your actions.
import thunkMiddleware from 'redux-thunk';
// the logger middleware is useful for inspecting data flow
import createLogger from 'redux-logger';
// Here, your new vital middleware is imported
import { myNetMiddleware } from '<your written middleware>';
/* vanilla index component */
import _Index from './components';
/* Redux reducers */
import reducers from './reducers';
/* Redux actions*/
import actionCreators from './actions/auth';
/* create store */
const store = createStore(
reducers,
applyMiddleware(
thunkMiddleware,
myNetMiddleware
)
);
/* Taint that component with store and actions */
/* If all goes well props should have 'auth', after we are done */
const Index = connect( (state) => {
const { auth } = state;
return {
auth
};
}, (dispatch) => {
return bindActionCreators(actionCreators, dispatch);
})(_Index);
const provider = (
<Provider store={store}>
<Index />
</Provider>
);
const entryElement = document.getElementById('app');
ReactDOM.render(provider, entryElement);
All of this implies you already set up a pipeline using webpack,rollup or something, to transpile from es2015 and react, to vanilla js.
Consider using the new thunk API
export const load = createAsyncThunk(
'example/api',
async (arg, thunkApi) => {
const response = await fetch('http://example.api.com/api')
if (response.status === 200) {
const json = await response.json()
return json
},
)
Also, in the new redux template application, actions are part of the reducer/slice, and you can use extraReducers to response to events related to the async action status. It is much simpler using redux this way.
See documentation of async thunk here: https://redux.js.org/usage/writing-logic-thunks

Resources