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.
Related
I am just writing up a a small site that fetches list of repositories for a given user and displays them in a grid.
To achieve it i am using probably an overkill combination of redux, redux-sagas, axios and redux-hook.
First thing i do is i have a httpClient that does the async call to fetch the repos which returns array of objects [{},{},{}]
import axios from "axios";
export const getProjects = async () => {
return await axios.get("https://api.github.com/users/xxx/repos", {
headers: {
"Content-type": "application/json",
},
});
};
inside my Container component which is loaded when the app mounts i dispatch a action to trigger the redux cycle:
const dispatch = useDispatch();
useEffect(() => {
dispatch(getProjectsRequest());
}, []);
the Action:
export const getProjectsRequest = () => ({
type: ActionTypes.GET_PROJECTS_REQUEST,
})
This is then captured by my saga where i yield my httpCLient that return the array of objects and passes the payload onto getProjectsSuccess(result.data) which is :
export const getProjectsSuccess = (projects) => ({
type: ActionTypes.GET_PROJECTS_SUCCESS,
payload: {
projects
}
})
SAGA:
import { call, put, takeEvery, fork } from "redux-saga/effects";
import { ActionTypes } from "../actionTypes";
import * as actionProjects from "../actions/projectsAction";
import * as http from "../../api/httpClient";
// Worker Saga
function* fetchProjects() {
try {
const result = yield call(http.getProjects);
yield put(actionProjects.getProjectsSuccess(result.data));
} catch (error) {
console.log(error);
yield put({ type: "GET_PROJECTS_FAILED", message: error.message });
}
}
function* watchGetProjectsRequest() {
yield takeEvery(ActionTypes.GET_PROJECTS_REQUEST, fetchProjects);
}
const projectsSagaResult = [fork(watchGetProjectsRequest)];
export default projectsSagaResult;
This is the captured in my reducer and updates the state accordingly with array of objects:
case ActionTypes.GET_PROJECTS_SUCCESS: {
return {
isLoading: false,
...action.payload,
};
}
FINALLY:
In my projects.js component where i am trying do loop and display all the projects from GITHUB user i use const { projects } = useSelector((state) => state.gitHubPortfolio)
so that i can access the state slice and filter over it like so:
const test = projects.filter(x => {return x.name === "m" })
This instantly throws a error:
Uncaught TypeError: Cannot read properties of undefined (reading 'filter')
But when i step through the code in the browser i can do this without the error so the useSelector fetches array of objects from the state.
Now i the console i can simply filter projects array whilst inn debugger mode like so:
AT LAST
I have no idea why i cant filter through the projects array inn my code, but it seems to me like its some PROMISE issue it might be that the projects are not set before i am trying to filter them i really have no idea.
I'm working on a React Native app. I have a signup screen which has a button, onclick:
const handleClick = (country: string, number: string): void => {
dispatch(registerUser({ country, number }))
.then(function (response) {
console.log("here", response);
navigation.navigate(AuthRoutes.Confirm);
})
.catch(function (e) {
console.log('rejected', e);
});
};
The registerUser function:
export const registerUser = createAsyncThunk(
'user/register',
async ({ country, number }: loginDataType, { rejectWithValue }) => {
try {
const response = await bdzApi.post('/register', { country, number });
return response.data;
} catch (err) {
console.log(err);
return rejectWithValue(err.message);
}
},
);
I have one of my extraReducers that is indeed called, proving that it's effectively rejected.
.addCase(registerUser.rejected, (state, {meta,payload,error }) => {
state.loginState = 'denied';
console.log(`nope : ${JSON.stringify(payload)}`);
})
But the signup component gets processed normally, logging "here" and navigating to the Confirm screen. Why is that?
A thunk created with createAsyncThunk will always resolve but if you want to catch it in the function that dispatches the thunk you have to use unwrapResults.
The thunks generated by createAsyncThunk will always return a resolved promise with either the fulfilled action object or rejected action object inside, as appropriate.
The calling logic may wish to treat these actions as if they were the original promise contents. Redux Toolkit exports an unwrapResult function that can be used to extract the payload of a fulfilled action or to throw either the error or, if available, payload created by rejectWithValue from a rejected action:
import { unwrapResult } from '#reduxjs/toolkit'
// in the component
const onClick = () => {
dispatch(fetchUserById(userId))
.then(unwrapResult)
.then(originalPromiseResult => {})
.catch(rejectedValueOrSerializedError => {})
}
I am currently trying to load my product data into redux, but so far I cant seem to pass the product information returned from firestore into the reducer.
Index.js -> load first 10 products from firestore soon after store was created.
store.dispatch(getAllProducts)
action/index.js
import shop from '../api/shop'
const receiveProducts = products => ({
type: types.RECEIVE_PRODUCTS
products
})
const getAllProducts = () => dispatch => {
shop.getProducts(products => {
dispatch(receiveProducts)
})
}
shop.js
import fetchProducts from './firebase/fetchProducts'
export default {
getProducts: (cb) => cb(fetchProducts())
}
fetchProducts.js
const fetchProducts = async() => {
const ProductList = await firebase_product.firestore()
.collection('store_products').limit(10)
ProductList.get().then((querySnapshot) => {
const tempDoc = querySnapshot.docs.map((doc) => {
return { id: doc.id, ...doc.data() }
})
}).catch(function (error) {
console.log('Error getting Documents: ', error)
})
}
In product reducers
const byId = (state={}, action) => {
case RECEIVE_PRODUCTS:
console.log(action); <- this should be products, but it is now promise due to aysnc function return?
}
I can get the documents with no issues (tempDocs gets the first 10 documents without any issue.) but I am not able to pass the data back into my redux. If I were creating normal react app, I would add a loading state when retrieving the documents from firestore, do I need to do something similar in redux as well ?
Sorry if the code seems messy at the moment.
fetchProducts is an async function so you need to wait for its result before calling dispatch. There are a few ways you could do this, you could give fetchProducts access to dispatch via a hook or passing dispatch to fetchProducts directly.
I don't quite understand the purpose of shop.js but you also could await fetchProducts and then pass the result of that into dispatch.
A generalized routine I use to accomplish exactly this:
const ListenGenerator = (sliceName, tableName, filterArray) => {
return () => {
//returns a listener function
try {
const unsubscribe = ListenCollectionGroupQuery(
tableName,
filterArray,
(listenResults) => {
store.dispatch(
genericReduxAction(sliceName, tableName, listenResults)
);
},
(err) => {
console.log(
err + ` ListenGenerator listener ${sliceName} ${tableName} err`
);
}
);
//The unsubscribe function to be returned includes clearing
// Redux entry
const unsubscriber = () => {
//effectively a closure
unsubscribe();
store.dispatch(genericReduxAction(sliceName, tableName, null));
};
return unsubscriber;
} catch (err) {
console.log(
`failed:ListenGenerator ${sliceName} ${tableName} err: ${err}`
);
}
};
};
The ListenCollectionGroupQuery does what it sounds like; it takes a tableName, an array of filter/.where() conditions, and data/err callbacks.
The genericReduxAction pretty much just concatenates the sliceName and TableName to create an action type (my reducers de-construct action types similarly). The point is you can put the dispatch into the datacallback.
Beyond this, you simply treat Redux as Redux - subscribe, get, etc just as if the data were completely local.
I am using redux and redux-saga in my project. Right now using WebSocket I have a problem calling a FETCH_SUCCESS redux action inside a callback of socket response. I tried making the callback a generator as well but didn't work as well.
function* websocketSaga() {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
const token = yield select(selectToken);
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
console.log(response); // here is the proper response, it works
put({
type: FETCH_SUCCESS, // here the FETCH_SUCCESS action is not called
payload: response.dataResponse,
});
});
...
....
}
);
}
Or maybe this WebSocket should be implemented in a completely different way in redux-saga?
You won't be able to use yield put inside a callback function. Stompjs knows nothing about sagas, so it doesn't know what it's supposed to do when given a generator function.
The simplest approach, though not necessarily the best, is to go directly to the redux store in the callback, and dispatch the action without involving redux-saga. For example:
import store from 'wherever you setup your store'
// ...
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
store.dispatch({
type: FETCH_SUCCESS,
payload: response.dataResponse,
});
});
If you'd like to use a more redux-saga-y approach, I would recommend wrapping the subscription in an event channel. Event channels take a callback-based API and turn it into something that you can interact with using redux-saga's effects such as take
Here's how you might create the event channel:
import { eventChannel } from 'redux-saga';
function createChannel(token) {
return eventChannel(emitter => {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
emitter(response); // This is the value which will be made available to your saga
});
}
);
// Returning a cleanup function, to be called if the saga completes or is cancelled
return () => stomp.disconnect();
});
}
And then you'd use it like this:
function* websocketSaga() {
const token = yield select(selectToken);
const channel = createChannel(token);
while (true) {
const response = yield take(channel);
yield put({
type: FETCH_SUCCESS,
payload: response.dataResponse,
});
}
}
Promise should be the perfect fit. Just wrap the callback related code in a promise and resolve it in the callback function. After that use the yield to get the data from the promise. I have modified your code with the Promise below.
function* websocketSaga() {
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
const stomp = Stomp.over(socket);
const token = yield select(selectToken);
const p = new Promise((resolve, reject) => {
stomp.connect(
{
Authorization: `Bearer ${token}`,
},
frame => {
stomp.subscribe('/queue/data', message => {
const response = JSON.parse(message.body);
console.log(response); // here is the proper response, it works
resolve(response); // here resolve the promise, or reject if any error
});
...
....
}
);
});
try {
const response = yield p; // here you will get the resolved data
yield put({
type: FETCH_SUCCESS, // here the FETCH_SUCCESS action is not called
payload: response.dataResponse,
});
} catch (ex) {
// handle error here, with rejected value
}
}
I will give you another way of managing this: create a component connected to redux where you will handle the WS subscription. This component will not render anything to the UI but will be useful for handling redux store interactions.
The main idea is, don't put everything into redux-saga, try and split it into multiple parts to make it easier to maintain.
const socket = new SockJS(`${CONFIG.API_URL}/ws`);
function WSConnection(props) {
const {token, fetchDone} = props;
const [stomp, setStomp] = React.useState();
const onMessage = React.useCallback(message => {
const response = JSON.parse(message.body);
fetchDone(response.dataResponse);
}, [fetchDone]);
const onConnect = React.useCallback(frame => {
const subscription = stomp.subscribe('/queue/data', onMessage);
// cleanup subscription
return () => subscription.unsubscribe();
}, [stomp, onMessage]);
const onError = React.useCallback(error => {
// some error happened, handle it here
}, []);
React.useEffect(() => {
const header = {Authorization: `Bearer ${token}`};
stomp.connect(header, onConnect, onError);
// cleanup function
return () => stomp.disconnect();
}, [stomp])
React.useEffect(() => {
setStomp(Stomp.over(socket));
}, []);
return null;
}
const mapStateToProps = state => ({
... // whatever you need from redux store
});
const mapDispatchToProps = dispatch => ({
... // whatever actions you need to dispatch
});
export default connect(mapStateToProps, mapDispatchToProps)(WSConnection);
You can also take it a step further and extract the stomp logic into another file and reuse it wherever you will need it.
It's not wrong to put everything into redux-saga but it's a nice alternative to handle WS connections inside components connected to redux (and easier to understand to people who are not completely familiar with redux-saga and channels etc).
I have the same stack over the years and only recently I faced websockets over Stomp client.
None of the above solutions doesn't work for me both technically and mentally
Reasons:
I don't like channels with Stomp because the only way to manipulate connections in more surgical way you have to use global state object (for me - it's redux). It doesn't seems right even if you storing only random generated IDS (with unsubscribe function it will be... read more here about store serialization
the way with container another pain in the ... (you know where). Again redux and a lot of under-the-hood functionality used without any reason
another way with promises: again without storing helpful connection info and some DI by using promises inside generators. This narrows down the implementation choice
So:
I need to have connection info (I decided to use state but not in: redux, component state. Singleton state). Stomp doesn't force you to place ID but I do because I want to manage connections by myself
I need one entry point without: promises, iterators and a lot of things that will be pain for future-me. One place to "rule them all" (as I want)
- activate: login
- deactivate: logout
- subscribe: componentDidMount
- unsubscribe: componentWillUnmount
DI by request in one place (passing store.dispatch to constructor only if need it) // main topic of the question
And I wrote this implementation that perfectly works for me:
import SockJS from 'sockjs-client';
import {
Client,
IMessage,
messageCallbackType,
StompHeaders,
} from '#stomp/stompjs';
import { Action, Dispatch } from 'redux';
type ConnectionId = string;
interface IServiceConfig {
url: string;
dispatch?: Dispatch;
}
export default class Stomp {
serviceConfig: IServiceConfig = {
dispatch: null,
url: null,
};
ids: ConnectionId[] = [];
stomp: Client;
constructor(config: IServiceConfig) {
this.serviceConfig = { ...config };
this.stomp = new Client();
this.stomp.webSocketFactory = () => {
return (new SockJS(config.url));
};
}
alreadyInQueue = (id: ConnectionId): boolean => {
return Boolean(this.ids.find(_id => id === _id));
};
subscribeByDispatchAction = (
destination: string,
callback: (message: IMessage) => Action,
headers: StompHeaders & {
id: ConnectionId;
},
): void => {
const alreadyInQueue = this.alreadyInQueue(headers.id);
if (!alreadyInQueue) {
this.stomp.subscribe(
destination,
(message) => {
this.serviceConfig.dispatch(callback(message));
},
headers,
);
this.ids.push(headers.id);
return;
}
console.warn(`Already in queue #${headers.id}`);
};
subscribe = (
destination: string,
callback: messageCallbackType,
headers: StompHeaders & {
id: ConnectionId;
},
): void => {
const alreadyInQueue = this.alreadyInQueue(headers.id);
if (!alreadyInQueue) {
this.stomp.subscribe(
destination,
(message) => callback(message),
headers,
);
this.ids.push(headers.id);
this.logState('subscribe');
return;
}
console.warn(`Failed to subscribe over Socks by #${headers.id}`);
};
unsubscribe = (id: ConnectionId, headers?: StompHeaders): void => {
this.stomp.unsubscribe(id, headers);
this.ids.splice(this.ids.indexOf(id), 1);
};
activate = (): void => {
this.stomp.activate();
};
deactivate = (): void => {
if (this.ids.length === 0) {
this.stomp.deactivate();
return;
}
for (let i = 0; i < this.ids.length; i++) {
this.unsubscribe(this.ids[i]);
}
/**
* it seems like it's overkil but
* for me it works only if i do all
* the things as you see below
* - stomp deactivation
* - closing webSockets manually by using native constant // sockjs-client
* - closing webSockets instance by using returned value fron factory
*/
this.stomp.deactivate();
this.stomp.webSocket.close(
this.stomp.webSocket.CLOSED,
);
this.stomp.webSocketFactory().close();
};
getAllIds = (): readonly ConnectionId[] => {
return this.ids;
};
// debug method
logState = (method: string): void => {
/* eslint-disable */
console.group(`Stomp.${method}`);
console.log('this', this);
console.log('this.ids', this.getAllIds());
console.log('this.stomp', this.stomp);
console.groupEnd();
/* eslint-enable */
};
}
My configuration file
import { store } from '~/index';
import Stomp from '~/modules/_Core/services/Stomp';
import appConfig from '~/modules/Common/services/appConfig';
export const StompService = new Stomp({
dispatch: store?.dispatch,
url: `${appConfig.apiV1}/websocket`,
});
I hope that it will help someone
I am working with React + Redux application that uses a third party SDK to connect to a websocket, authenticate with a service, and send and receive data. Here are some examples of what might be done with the SDK:
import SDK from 'third-party';
const client = SDK.init(...);
client.connect();
client.on('auth-challenge', callback => {
// Retrieve auth token from back-end
});
client.on('ready', () => {
client.loadData().then(data => {
// do something with this
});
});
Might it be possible to store this data in my Redux store, or to load the auth token using Sagas and take an action on the SDK once the data is available?
I can imagine that I could import my store into this file and use store.dispatch() to, for example, request a token (via Saga), but how do I know when that token has loaded? Is this something that I simply need to make direct API calls with?
I suggest to put the async part as a promise into the componentDidMount method of one of the connected components and call the dispatcher as the token is received.
import { askForToken } from '../my-helpers/sdk-helper;
class SomeParentComponentsContainer extends Component {
componentDidMount(){
const { dispatch } = this.props;
dispatch({ type: 'GET_TOKEN' })
// async part. Drop it if you use sagas.
askForToken()
.then(token => {
dispatch({ type: 'GET_TOKEN__SUCCESS', payload: { token } })
})
// ----
}
someMethodWhichNeedsTheToken = () => {
// this is available in any connected component now from store
const { sdkToken } = this.props;
....
}
...
}
const mapDispatchToProps = state => ({
sdkToken: state && state.sdkToken
})
export default connect(mapDispatchToProps)(SomeParentComponentsContainer);
Second option is if you use sagas, just keep dispatch({ type: 'GET_TOKEN' }) part in componentDidMount and saga will do the rest.
sagas.js
import { call, put, takeEvery, takeLatest } from 'redux-saga/effects'
import { askForToken } from '../my-helpers/sdk-helper;
function* fetchToken(action) {
try {
const user = yield call(askForToken);
yield put({type: "GET_TOKEN__SUCCESS", token });
} catch (e) {
yield put({type: "GET_TOKEN__FAILS", message: e.message});
}
}
function* mySaga() {
yield takeEvery("GET_TOKEN", fetchToken);
}
See sagas documentation on how to set up the middleware to make saga work.