server side redux-saga initial state - reactjs

I'm using react-boilerplate for my App (Using SSR branch). For some reason we need server side rendering. Also we have some rest API and we need to call one API before all API (for register something). I thinks for initial state I need to call this (first API that need data for registration ) API on the server and save response data into store and return store to client. In react-boilerplate for create store:
/**
* Create the store with asynchronously loaded reducers
*/
import { createStore, applyMiddleware, compose } from 'redux';
import { fromJS } from 'immutable';
import { routerMiddleware } from 'react-router-redux';
import createSagaMiddleware from 'redux-saga';
import createReducer from './reducers';
const sagaMiddleware = createSagaMiddleware();
export default function configureStore(initialState = {}, history) {
// Create the store with two middlewares
// 1. sagaMiddleware: Makes redux-sagas work
// 2. routerMiddleware: Syncs the location/URL path to the state
const middlewares = [
sagaMiddleware,
routerMiddleware(history),
];
const enhancers = [
applyMiddleware(...middlewares),
];
// If Redux DevTools Extension is installed use it, otherwise use Redux compose
/* eslint-disable no-underscore-dangle */
const composeEnhancers =
process.env.NODE_ENV !== 'production' &&
typeof window === 'object' &&
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ ?
window.__REDUX_DEVTOOLS_EXTENSION_COMPOSE__ : compose;
/* eslint-enable */
const store = createStore(
createReducer(),
fromJS(initialState),
composeEnhancers(...enhancers)
);
// Extensions
store.runSaga = sagaMiddleware.run;
store.asyncReducers = {}; // Async reducer registry
// Make reducers hot reloadable, see http://mxs.is/googmo
/* istanbul ignore next */
if (module.hot) {
module.hot.accept('./reducers', () => {
import('./reducers').then((reducerModule) => {
const createReducers = reducerModule.default;
const nextReducers = createReducers(store.asyncReducers);
store.replaceReducer(nextReducers);
});
});
}
return store;
}
and also for making initial store defined :
function renderAppToStringAtLocation(url, { webpackDllNames = [], assets, lang }, callback) {
const memHistory = createMemoryHistory(url);
const store = createStore({}, memHistory);
syncHistoryWithStore(memHistory, store);
const routes = createRoutes(store);
const sagasDone = monitorSagas(store);
store.dispatch(changeLocale(lang));
match({ routes, location: url }, (error, redirectLocation, renderProps) => {
if (error) {
callback({ error });
} else if (redirectLocation) {
callback({ redirectLocation: redirectLocation.pathname + redirectLocation.search });
} else if (renderProps) {
renderHtmlDocument({ store, renderProps, sagasDone, assets, webpackDllNames })
.then((html) => {
const notFound = is404(renderProps.routes);
callback({ html, notFound });
})
.catch((e) => callback({ error: e }));
} else {
callback({ error: new Error('Unknown error') });
}
});
}
and for filling the initial state I do some change:
async function fetches (hostname) {
const domain = hostname.replace('.myExample.com', '').replace('www.', '');
const options = {
method: 'GET',
headers: {
'Content-Type': 'application/x-www-form-urlencoded',
'Accept': 'application/example.api.v1.0+json',
}
};
const uri ='https://api.example.com/x/' + domain + '/details';
const shopDetail = await fetch(uri, options);
return shopDetail.json();
}
function renderAppToStringAtLocation(hostname ,url, { webpackDllNames = [], assets, lang }, callback) {
const memHistory = createMemoryHistory(url);
console.log('url :', hostname);
fetches(hostname).then( data => {
const store = createStore(data, memHistory);
syncHistoryWithStore(memHistory, store);
const routes = createRoutes(store);
const sagasDone = monitorSagas(store);
store.dispatch(changeLocale(lang));
match({ routes, location: url }, (error, redirectLocation, renderProps) => {
if (error) {
callback({ error });
} else if (redirectLocation) {
callback({ redirectLocation: redirectLocation.pathname + redirectLocation.search });
} else if (renderProps) {
renderHtmlDocument({ store, renderProps, sagasDone, assets, webpackDllNames })
.then((html) => {
const notFound = is404(renderProps.routes);
callback({ html, notFound });
})
.catch((e) => callback({ error: e }));
} else {
callback({ error: new Error('Unknown error') });
}
});
});
and then in console I get this error:
Unexpected properties "code", "data" found in initialState argument
passed to createStore. Expected to find one of the known reducer
property names instead: "route", "global", "language". Unexpected
properties will be ignored.
how to fix it?

I thinks for initial state I need to call this (first API that need data for registration ) API on the server and save response data into store and return store to client
There are two different solutions, dependent on side, on which API call should be performed.
If it's just server-side call, HTTP response and subsequent SSR phase should be delayed, until fetch is done. It can be solved in express by wrapping into middleware function. Usually such schema is used when integrating with external authorization services (Auth0, Passport, etc), but it's better to wrap authorization information into JWT and not into INITIAL_STATE.
If API call can be done from client side, just use redux-saga. It can spawn dedicated process, which will catch all redux actions before API call is done, and then play them respectively. In this case initialState object should contain structure-like fields without data, which will be filled later after API call.

Related

Request failed with status code 404 (REDUX)

Can't seem to pull data from database but according Redux DevTools the actions PASSWORD_LIST_REQUEST seems to be firing. What is going wrong here? I checked my original route, it should be going towards "api/passwords" in the server, is this a backend problem or Redux problem?
I checked the backend url routes with postman, so thats all good.
ACTIONS
export const listPasswords = () => async (dispatch) => {
try {
dispatch({ type: PASSWORD_LIST_REQUEST });
const { data } = await axios.get("/api/passwords");
dispatch({
type: PASSWORD_LIST_SUCCESS,
payload: data,
});
} catch (error) {
dispatch({
type: PASSWORD_LIST_FAIL,
payload:
error.response && error.response.data.message
? error.response.data.message
: error.message,
});
}
};
REDUCERS
export const passwordListReducer = (state = { passwords: [] }, action) => {
switch (action.type) {
case PASSWORD_LIST_REQUEST:
return {
loading: true,
passwords: [],
};
case PASSWORD_LIST_SUCCESS:
return { passwords: action.payload, loading: false };
case PASSWORD_LIST_FAIL:
return { loading: false, error: action.payload };
default:
return state;
}
};
STORE
import { createStore, combineReducers, applyMiddleware } from "redux";
import thunk from "redux-thunk";
import { composeWithDevTools } from "redux-devtools-extension";
import { passwordListReducer } from "./reducers/passwordReducers";
const reducer = combineReducers({ passwordList: passwordListReducer });
const initialState = {};
const middleware = [thunk];
const store = createStore(
reducer,
initialState,
composeWithDevTools(applyMiddleware(...middleware))
);
export default store;
Server
import passwordRoutes from "./routes/passwordRoutes.js";
dotenv.config();
connectDB();
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.get("/", (req, res) => {
res.send("API is running");
});
app.use("/api/passwords", passwordRoutes);
app.use(notFound);
app.use(errorHandler);
const PORT = process.env.PORT || 5000;
app.listen(
PORT,
console.log(`Server running in ${process.env.NODE_ENV} mode on port ${PORT}`)
);
Seems like your call to endpoint might be going to the wrong url here
const { data } = await axios.get("/api/passwords");
The above axios call would point to your app url, say you app is running on localhost:3000, an API call would be made to http://localhost:3000/api/password by default.
If your app and back end run on different ports on your localhost, you have to set the domain on the axios call. From you code sample, I see you set you APIs to run from port 5000.
const { data } = await axios.get("http://localhost:5000/api/passwords");
Tip
If you don't want to keep repeating the base url, you can create an axios instance
export const request = axios.create({baseUrl: "http://localhost:5000"});
Then make your requests this way
import { request } from "path to request module";
...
const { data } = await request.get("/api/passwords");
...
I needed a proxy in package.json.
"proxy": "http://localhost:5000",

Laravel lighthouse current user is null via next apollo

I have a fresh copy of laravel with sanctum and lighthouse. When I do the login route via axios, everything works as expected. After logging in via axios, I added a lazyquery to attempt to query some guarded fields but I get unauthenticated. I am not sure why and it has been three days I've been dealing with this. I'd really appreciate your help.
This works
useEffect(() => {
axios.defaults.withCredentials = true;
// get the token from the server
axios.get(`http://api.newods.test/sanctum/csrf-cookie`).then(function (resolve){
// try login with the user
axios.post('http://api.newods.test/api/login', {
email: 'test#test.com',
password: 'test'
}).then(function (resolve) {
console.log(`logged in ${resolve.data}`);
axios
.get("http://api.newods.test/api/gated", { withCredentials: true })
.then(function (resolve) {
console.log(`gated ${resolve.data}`);
axios
.get("http://api.newods.test/api/logout", {
withCredentials: true,
})
.then(function (resolve) {
console.log(`logged out ${resolve.data}`);
axios
.get("http://api.newods.test/api/gated", {
withCredentials: true,
})
.then(function (resolve) {
console.log(
`trying to get to gated after logging out ${resolve.data}`
);
});
});
});
});
});
}, []);
But when I cut it short and change to this, I get unauthenticated
const HELLO = gql\`
query hello {
hello
}
`;
function Home() {
const [hello, { loading, data }] = useLazyQuery(HELLO);
useEffect(() => {
axios.defaults.withCredentials = true;
// get the token from the server
axios.get(`http://api.newods.test/sanctum/csrf-cookie`).then(function (resolve){
// try login with the user
axios.post('http://api.newods.test/api/login', {
email: 'test#test.com',
password: 'test'
}).then(function (resolve) {
console.log('logged in');
});
});
}, []);
return (
<div className="container">
<div>Index</div>
<button onClick={() => hello()}>
Click to hello world
</button>
<p>{data && data.hello || ''}</p>
</div>
);
}
export default withApollo(Home);
And that returns unauthenticated when I add the #guard directive and I see the token from the axios login request is in the headers... I am not sure what I am missing here I'd greatly appreciate your help.
schema.graphql
type Query {
users: [User!]! #paginate(defaultCount: 10)
user(id: ID #eq): User #find
hello: String! #guard
me: User #auth
}
.env
SESSION_DRIVER=cookie
SESSION_LIFETIME=120
SESSION_DOMAIN=.newods.test
SANCTUM_STATEFUL_DOMAINS=newods.test:3000
config/cors.php
return [
'paths' => ['api/*', 'sanctum/csrf-cookie', 'graphql'],
'allowed_methods' => ['*'],
'allowed_origins' => ['*'],
'allowed_origins_patterns' => [],
'allowed_headers' => ['*'],
'exposed_headers' => [],
'max_age' => 0,
'supports_credentials' => true,
];
config/lighthouse
'route' => [
/*
* The URI the endpoint responds to, e.g. mydomain.com/graphql.
*/
'uri' => '/graphql',
/*
* Lighthouse creates a named route for convenient URL generation and redirects.
*/
'name' => 'graphql',
/*
* Beware that middleware defined here runs before the GraphQL execution phase,
* make sure to return spec-compliant responses in case an error is thrown.
*/
'middleware' => [
\Nuwave\Lighthouse\Support\Http\Middleware\AcceptJson::class,
// Logs in a user if they are authenticated. In contrast to Laravel's 'auth'
// middleware, this delegates auth and permission checks to the field level.
\Nuwave\Lighthouse\Support\Http\Middleware\AttemptAuthentication::class,
],
/*
* The `prefix` and `domain` configuration options are optional.
*/
//'prefix' => '',
//'domain' => '',
],
In my next app with apollo
create.js
import { ApolloClient } from 'apollo-client';
import { InMemoryCache } from 'apollo-cache-inmemory';
import { createHttpLink } from 'apollo-link-http';
import { setContext } from 'apollo-link-context';
import Cookies from 'js-cookie';
import { serverUrl } from '../config';
export default function createApolloClient(initialState, ctx) {
// The `ctx` (NextPageContext) will only be present on the server.
// use it to extract auth headers (ctx.req) or similar.
const authLink = setContext((_, { headers }) => {
// get the authentication token from local storage if it exists
const token = Cookies.get("XSRF-TOKEN");
// console.log(`token is ${token}`);
// return the headers to the context so httpLink can read them
return {
headers: {
...headers,
"Access-Control-Allow-Credentials": true,
...(token ? { authorization: `X-XSRF-TOKEN=${token}` } : {}),
},
};
});
const httpLink = createHttpLink({
uri: serverUrl,
credentials: 'same-origin',
});
return new ApolloClient({
ssrMode: Boolean(ctx),
link: authLink.concat(httpLink),
connectToDevTools: true,
cache: new InMemoryCache().restore(initialState),
});
}
withApollo.js
import React from "react";
import Head from "next/head";
import { ApolloProvider } from "#apollo/react-hooks";
import { ApolloClient } from "apollo-client";
import { InMemoryCache } from "apollo-cache-inmemory";
import { HttpLink } from "apollo-link-http";
import fetch from "isomorphic-unfetch";
import createApolloClient from './create';
let apolloClient = null;
/**
* Creates and provides the apolloContext
* to a next.js PageTree. Use it by wrapping
* your PageComponent via HOC pattern.
* #param {Function|Class} PageComponent
* #param {Object} [config]
* #param {Boolean} [config.ssr=true]
*/
export function withApollo(PageComponent, { ssr = true } = {}) {
const WithApollo = ({ apolloClient, apolloState, ...pageProps }) => {
const client = apolloClient || initApolloClient(apolloState);
return (
<ApolloProvider client={client}>
<PageComponent {...pageProps} />
</ApolloProvider>
);
};
// Set the correct displayName in development
if (process.env.NODE_ENV !== "production") {
const displayName =
PageComponent.displayName || PageComponent.name || "Component";
if (displayName === "App") {
console.warn("This withApollo HOC only works with PageComponents.");
}
WithApollo.displayName = `withApollo(${displayName})`;
}
if (ssr || PageComponent.getInitialProps) {
WithApollo.getInitialProps = async (ctx) => {
const { AppTree } = ctx;
// Initialize ApolloClient, add it to the ctx object so
// we can use it in `PageComponent.getInitialProp`.
const apolloClient = (ctx.apolloClient = initApolloClient(
{},
ctx.req.headers.cookie
));
// Run wrapped getInitialProps methods
let pageProps = {};
if (PageComponent.getInitialProps) {
pageProps = await PageComponent.getInitialProps(ctx);
}
// Only on the server:
if (typeof window === "undefined") {
// When redirecting, the response is finished.
// No point in continuing to render
if (ctx.res && ctx.res.finished) {
return pageProps;
}
// Only if ssr is enabled
if (ssr) {
try {
// Run all GraphQL queries
const { getDataFromTree } = await import("#apollo/react-ssr");
await getDataFromTree(
<AppTree
pageProps={{
...pageProps,
apolloClient,
}}
/>
);
} catch (error) {
// Prevent Apollo Client GraphQL errors from crashing SSR.
// Handle them in components via the data.error prop:
// https://www.apollographql.com/docs/react/api/react-apollo.html#graphql-query-data-error
console.error("Error while running `getDataFromTree`", error);
}
// getDataFromTree does not call componentWillUnmount
// head side effect therefore need to be cleared manually
Head.rewind();
}
}
// Extract query data from the Apollo store
// #ts-ignore
const apolloState = apolloClient.cache.extract();
return {
...pageProps,
apolloState,
};
};
}
return WithApollo;
}
/**
* Always creates a new apollo client on the server
* Creates or reuses apollo client in the browser.
* #param {Object} initialState
*/
function initApolloClient(initialState = {}, cookie = "") {
// Make sure to create a new client for every server-side request so that data
// isn"t shared between connections (which would be bad)
if (typeof window === "undefined") {
return createApolloClient(initialState, cookie);
}
// Reuse client on the client-side
if (!apolloClient) {
// #ts-ignore
apolloClient = createApolloClient(initialState);
}
return apolloClient;
}
I have a very similar architecture, but using Vue. From comparing your code against my working implementation, I think the majority of your problems are in create.js.
I don't know much about js-cookie, but this is how I get the XSRF-TOKEN, and decode it.
let token = RegExp('XSRF-TOKEN[^;]+').exec(document.cookie)
token = decodeURIComponent(token ? token.toString().replace(/^[^=]+./, '') : '')
Then, in your setContext, you need to set the header as follows.
return {
headers: {
...headers,
'X-XSRF-TOKEN': token,
}
}
Also, I had trouble with credentials: 'same-origin' even though I'm using a subdomain. Therefore I would suggest:
const httpLink = createHttpLink({
uri: serverUrl,
credentials: 'include',
})

"this.getClient(...).watchQuery is not a function" - remote schema stitching with Apollo 2 / Next.js

So I'm attempting to stitch multiple remote GraphCMS endpoints together on the clientside of a Next.js app, and after trying/combining about every example on the face of the internet, I've gotten it to a place that's worth asking about. My error:
TypeError: this.getClient(...).watchQuery is not a function at GraphQL.createQuery
github repo here, where you can see this initApollo.js in context:
import { ApolloClient } from 'apollo-client'
import {
makeRemoteExecutableSchema,
mergeSchemas,
introspectSchema
} from 'graphql-tools'
import { HttpLink } from 'apollo-link-http'
import { InMemoryCache } from 'apollo-cache-inmemory'
import fetch from 'node-fetch'
import { Observable, ApolloLink } from 'apollo-link'
import { graphql, print } from 'graphql'
import { createApolloFetch } from 'apollo-fetch'
let apolloClient = null
if (!process.browser) {
global.fetch = fetch
}
const PRIMARY_API = 'https://api.graphcms.com/simple/v1/cjfipt3m23x9i0190pgetwf8c'
const SECONDARY_API = 'https://api.graphcms.com/simple/v1/cjfipwwve7vl901427mf2vkog'
const ALL_ENDPOINTS = [PRIMARY_API, SECONDARY_API]
async function createClient (initialState) {
const AllLinks = ALL_ENDPOINTS.map(endpoint => {
return new HttpLink({
uri: endpoint,
fetch
})
})
const allSchemas = []
for (let link of AllLinks) {
try {
allSchemas.push(
makeRemoteExecutableSchema({
schema: await introspectSchema(link),
link
})
)
} catch (e) {
console.log(e)
}
}
const mergedSchema = mergeSchemas({
schemas: allSchemas
})
const mergedLink = operation => {
return new Observable(observer => {
const { query, variables, operationName } = operation
graphql(mergedSchema, print(query), {}, {}, variables, operationName)
.then(result => {
observer.next(result)
observer.complete()
})
.catch(e => observer.error(e))
})
}
return new ApolloClient({
connectToDevTools: process.browser,
ssrMode: !process.browser,
link: mergedLink,
cache: new InMemoryCache().restore(initialState || {})
})
}
export default function initApollo (initialState) {
if (!process.browser) {
return createClient(initialState)
}
if (!apolloClient) {
apolloClient = createClient(initialState)
}
console.log('\x1b[37m%s\x1b[0m', apolloClient)
return apolloClient
}
I'm getting useful data all the way up into the .then() inside the Observable, where I can log the result
This is a shot in the dark, but initApollo isn't async so it returns a promise (not an ApolloClient object) which is then being passed into client prop of the ApolloProvider. watchQuery doesn't exist as a function on the Promise type, hence the error.
I think if you make initApollo async and then await those calls or find a way to make client creation synchronous, you should be able to address this issue.

Testing fetch action in react/redux app

Im starting with unit testing and Jest. What I want is to test the action's response after fetching some resources from the db.
This is the action code:
export function loadPortlets() {
return function(dispatch) {
return portletApi.getAllPortlets().then(response => {
dispatch(loadPortletsSuccess(response));
dispatch(hideLoading());
}).catch(error => {
dispatch({ type: null, error: error });
dispatch(hideLoading());
throw(error);
});
};
}
This code is fetching data from:
static getAllPortlets() {
return fetch(`${API_HOST + API_URI}?${RES_TYPE}`)
.then(response =>
response.json().then(json => {
if (!response.ok) {
return Promise.reject(json);
}
return json;
})
);
}
And this is the test:
import configureMockStore from 'redux-mock-store';
import thunk from 'redux-thunk';
import fetch from 'isomorphic-fetch';
import fetchMock from 'fetch-mock';
import * as actions from '../portletActions';
import * as types from '../actionTypes';
const middlewares = [thunk];
const mockStore = configureMockStore(middlewares);
const mockResponse = (status, statusText, response) => {
return new window.Response(response, {
status: status,
statusText: statusText,
headers: {
'Content-type': 'application/json'
}
});
};
describe('async actions', () => {
afterEach(() => {
fetchMock.reset();
fetchMock.restore();
})
it('calls request and success actions if the fetch response was successful', () => {
window.fetch = jest.fn().mockImplementation(() =>
Promise.resolve(mockResponse(200, null, [{ portlets: ['do something'] }])));
const store = mockStore({ portlets: []});
return store.dispatch(actions.loadPortlets())
.then(() => {
const expectedActions = store.getActions();
expect(expectedActions[0]).toContain({ type: types.LOAD_PORTLETS_SUCCESS });
})
});
});
And this is the result of running the test:
FAIL src\actions\__tests__\portletActions.tests.js
● async actions › calls request and success actions if the fetch response was successful
expect(object).toContain(value)
Expected object:
{"portlets": [// here an array of objects], "type": "LOAD_PORTLETS_SUCCESS"}
To contain value:
{"type": "LOAD_PORTLETS_SUCCESS"}
at store.dispatch.then (src/actions/__tests__/portletActions.tests.js:56:34)
at <anonymous>
at process._tickCallback (internal/process/next_tick.js:188:7)
In the redux docs for this example (https://redux.js.org/recipes/writing-tests), they receive a result containing only the action types executed, but I'm getting the real data and the action inside the array.
So I'm not sure if the code is wrong, or the test, or both!
Thanks in advance, any help is highly appreciated!
You're testing too much with this unit test. I see you are using thunks it looks like so you can change your fetch to be passed as a module to the thunk and do something like this. I used jasmine but it's basically the same thing. You don't want to mock your store here just the action and dispatch. The point of the unit test should be to test the async action, not to test getting real data from the db or redux store interactions so you can stub all that out.
For reference configureStore would look like this...
const createStoreWithMiddleware = compose(
applyMiddleware(thunk.withExtraArgument({ personApi }))
)(createStore);
And the test case...
it('dispatches an action when receiving', done => {
const person = [{ firstName: 'Francois' }];
const expectedAction = {
type: ActionTypes.RECEIVED,
payload: {
people,
},
};
const dispatch = jasmine.createSpy();
const promise = Q.resolve(person);
const personApi = {
fetchPerson: jasmine
.createSpy()
.and.returnValue(promise),
};
const thunk = requestPerson();
thunk(dispatch, undefined, { personApi });
promise.then(() => {
expect(dispatch.calls.count()).toBe(2);
expect(dispatch.calls.mostRecent().args[0]).toEqual(expectedAction);
done();
});
});

Using redux-saga and redial Server Side

redux-saga
redial
Right now, I am trying to get the initial state of my application server side through Redial.
Redial triggers a pure object action, and redux-saga listens/awaits for that action, and then launches the async request.
But the problem is, Redial has no promises to resolve to when redux-saga is completed because it is dispatching a pure object.
Component
const redial = {
fetch: ({ dispatch }) => dispatch({ type: actionTypes.FETCH_START }),
};
export default class PostList extends Component {
render() {
const { posts } = this.props;
return (
<div>
{posts.map(post => <ListItem key={post.id} post={post} />)}
</div>
);
}
}
PostList.propTypes = {
posts: PropTypes.array.isRequired,
};
export default provideHooks(redial)(connect(mapStateToProps)(PostList));
Saga
export function *fetch() {
try {
yield put({ type: actionTypes.FETCH_START });
const response = yield call(fakeData);
yield put({ type: actionTypes.FETCH_SUCCESS, data: response.data });
yield put({ type: actionTypes.FETCH_PENDING });
} catch (e) {
yield put({ type: actionTypes.FETCH_FAIL });
}
}
export default function *loadPost() {
yield * takeLatest(actionTypes.FETCH_START, fetch);
}
export default function *rootSaga() {
yield [
fork(loadPost),
];
}
Is there a way to connect redial to redux-saga ?
I think it can be done in this way:
firstly, you need to add store in locals. (codes are taken from redial README)
const locals = {
path: renderProps.location.pathname,
query: renderProps.location.query,
params: renderProps.params,
// Allow lifecycle hooks to dispatch Redux actions:
dispatch,
store
};
Then you can create a Promise manually like this:
const redial = {
fetch: ({ store, dispatch }) => {
return new Promise((resolve, reject) => {
const unsubscribe = store.subscribe(()=>{
if (store.getState()...) { // monitor store state changing by your saga
resolve(...) //you probably dont need any result since your container can read them from store directly
unsubscribe();
}
if (store.getState()....error) {
reject(...)
unsubscribe();
}
});
dispatch({ type: actionTypes.FETCH_START }),
}
}
};
Those codes are just for demonstration, don't use them in production without proper testing.
I think there might be a more elegant way to monitor saga execution results than checking redux store state over and over until the state matches those if(...) statements, maybe you can run saga with redux store and external listeners, then those redial hooks wont need to know about your store structure.
There is a rather elegant way of doing this. First of all you need to create a registry for your saga tasks (remember that running the middleware's .run method returns a task descriptor):
export default class SagaTaskRegistry {
constructor() {
this._taskPromises = [];
}
addTask(task) {
if (!this._taskPromises) {
this._taskPromises = [];
}
this._taskPromises.push(task.done);
}
getPromise() {
return new Promise((resolve) => {
const promises = this._taskPromises;
if (!promises) {
resolve();
return;
}
this._taskPromises = undefined;
Promise.all(promises).then(resolve).catch(resolve);
}).then(() => {
const promises = this._taskPromises;
if (promises) {
return this.getPromise();
}
return undefined;
});
}
}
When you add new tasks to the saga middleware using .run, you will then call registryInstance.add(taskDescriptor). The SagaTaskRegistry will grab the promise for that task and add it to an array.
By calling getPromise, you will receive a promise which will resolve when all added tasks are finished. It will never be rejected, as you most likely wouldn't want failed fetches to result in a rejection - you still want to render your application with the error state.
And this is how you can combine it with redial:
import createSagaMiddleware from 'redux-saga';
import { applyMiddleware, createStore } from 'redux';
import rootReducer from 'your/root/reducer';
import yourSaga from 'your/saga';
const sagaMiddleware = createSagaMiddleware();
const middleWare = [sagaMiddleware];
const createStoreWithMiddleware = applyMiddleware(...middleWare)(createStore);
const store = createStoreWithMiddleware(rootReducer);
const sagaTaskRegistry = new SagaTaskRegistry();
const sagaTask = sagaMiddleware.run(yourSaga);
sagaTaskRegistry.addTask(sagaTask);
match({ routes, history }, (error, redirectLocation, renderProps) => {
const locals = {
path: renderProps.location.pathname,
query: renderProps.location.query,
params: renderProps.params,
dispatch: store.dispatch,
};
trigger('fetch', components, locals);
// Dispatching `END` will force watcher-sagas to terminate,
// which is required for the task promises to resolve.
// Without this the server would never render anything.
// import this from the `redux-saga` package
store.dispatch(END);
// The `SagaTaskRegistry` keeps track of the promises we have to resolve
// before we can render
sagaTaskRegistry.getPromise().then(...)
});
A component can now be decorated with a simple hook:
const hooks = {
fetch: ({ dispatch }) => {
dispatch(yourAction());
},
};
From here on out you can just use sagas as usual. This should give you the ability to do what you are trying. You can further abstract this to allow for dynamic registration of sagas across code-split chunks and other things. The task registry already works for these use-cases by checking for newly registered tasks since the last call to getPromise before actually resolving the promise.

Resources