I'm learning Redux-Saga and having a bit of trouble wrapping my head round the correct flow for connectng people to a chat service (Chatkit by Pusher) when they log in and disconnecting them on logout.
So far I have an "auth" saga which waits for a LOGIN_REQUEST action, logs in to a REST api using axios then stores a username and token in the store by calling a USER_SET action.
My question is, when the login happens and the credentials are stored, should I PUT a new action called something like CHAT_CONNECT which would kick off another saga to connect to Chatkit, or should I get the chat saga to listen to the LOGIN_SUCCESS being fired and act on that? Is there even any practical difference in these two approaches.
As a bonus question, what's the best way to receive and act on new websocket messages from Chatkit using Redux Sagas? Here's the boilerplate code for connecting and receiving events from chatkit.
chatManager
.connect()
.then(currentUser => {
currentUser.subscribeToRoom({
roomId: currentUser.rooms[0].id,
hooks: {
onNewMessage: message => {
console.log(`Received new message: ${message.text}`)
}
}
});
})
.catch(error => {
console.error("error:", error);
})
Regarding your first question:
My question is, when the login happens and the credentials are stored, should I PUT a new action called something like CHAT_CONNECT which would kick off another saga to connect to Chatkit, or should I get the chat saga to listen to the LOGIN_SUCCESS being fired and act on that?
With the information provided its difficult to decide which approach is ideal because either will accomplish the same functionality. The biggest difference I see between the two proposed approaches is the direction of dependency. You have two different "modules" (features, packages, ...whatever you call your chunks of code that handle a single responsiblity), lets call them log-in and connect-chat.
If you dispatch an action CHAT_CONNECT from within the log-in saga, your log-in module will be dependent to the connect-chat module. Presumably, the connect-chat action will live in the connect-chat module.
Alternatively, if your connect-chat saga waits for LOGIN_SUCCESS, then your connect-chat module will be dependent on your log-in module. Presumably, the LOGIN_SUCCESS will live in the log-in module.
There's nothing wrong with either approach. Which is best depends on your applications needs and functionality.
If you might want to connect to chat any other time then after successfully logging in, then it might make sense to dispatch CHAT_CONNECT from within your log-in saga. Because chat is no longer dependent on log in. There are several scenarios where either approach will work better than the other, but it really depends on how the rest of your application is set up.
Regarding your bonus questions:
One approach to hooking external events in redux-saga is accomplished via eventChannels. Docs: https://redux-saga.js.org/docs/api/#eventchannelsubscribe-buffer-matcher
There's a bit of boiler plate, but I found this approach makes testing easier and truly encapsulates external functionality. Here's a quick example of how I might hook up an event channel to the code snippet you provided:
export const createOnMessageChannel = () =>
eventChannel((emit) => {
chatManager
.connect()
.then(currentUser => {
currentUser.subscribeToRoom({
roomId: currentUser.rooms[0].id,
hooks: {
onNewMessage: message => emit({ message }),
}
});
})
.catch(error => emit({ error }));
return () => {
// Code to unsubscribe, e.g. chatManager.disconnet() ?
};
});
export function* onMessage({ message, error }) {
if (error) {
yield put(handleError(error));
return;
}
yield put(handleMessage(message));
}
// this is what you pass to your root saga
export function* createOnMessageSaga() {
// using call because this makes it easier to test
const channel = yield call(createOnMessageChannel);
if (!channel) return;
yield takeEvery(channel, onMessage);
}
Related
I have to pretty weird case to handle.
We have to few boxes, We can call some action on every box. When We click the button inside the box, we call some endpoint on the server (using axios). Response from the server return new updated information (about all boxes, not the only one on which we call the action).
Issue:
If user click submit button on many boxes really fast, the request call the endpoints one by one. It's sometimes causes errors, because it's calculated on the server in the wrong order (status of group of boxes depends of single box status). I know it's maybe more backend issue, but I have to try fix this on frontend.
Proposal fix:
In my opinion in this case the easiest fix is disable every submit button if any request in progress. This solution unfortunately is very slow, head of the project rejected this proposition.
What we want to goal:
In some way We want to queue the requests without disable every button. Perfect solution for me at this moment:
click first button - call endpoint, request pending on the server.
click second button - button show spinner/loading information without calling endpoint.
server get us response for the first click, only then we really call the second request.
I think something like this is huge antipattern, but I don't set the rules. ;)
I was reading about e.g. redux-observable, but if I don't have to I don't want to use other middleware for redux (now We use redux-thunk). Redux-saga it will be ok, but unfortunately I don't know this tool. I prepare simple codesandbox example (I added timeouts in redux actions for easier testing).
I have only one stupid proposal solution. Creating a array of data needs to send correct request, and inside useEffect checking if the array length is equal to 1. Something like this:
const App = ({ boxActions, inProgress, ended }) => {
const [queue, setQueue] = useState([]);
const handleSubmit = async () => { // this code do not work correctly, only show my what I was thinking about
if (queue.length === 1) {
const [data] = queue;
await boxActions.submit(data.id, data.timeout);
setQueue(queue.filter((item) => item.id !== data.id));
};
useEffect(() => {
handleSubmit();
}, [queue])
return (
<>
<div>
{config.map((item) => (
<Box
key={item.id}
id={item.id}
timeout={item.timeout}
handleSubmit={(id, timeout) => setQueue([...queue, {id, timeout}])}
inProgress={inProgress.includes(item.id)}
ended={ended.includes(item.id)}
/>
))}
</div>
</>
);
};
Any ideas?
I agree with your assessment that we ultimately need to make changes on the backend. Any user can mess with the frontend and submit requests in any order they want regardless how you organize it.
I get it though, you're looking to design the happy path on the frontend such that it works with the backend as it is currently.
It's hard to tell without knowing the use-case exactly, but there may generally be some improvements we can make from a UX perspective that will apply whether we make fixes on the backend or not.
Is there an endpoint to send multiple updates to? If so, we could debounce our network call to submit only when there is a delay in user activity.
Does the user need to be aware of order of selection and the impacts thereof? If so, it sounds like we'll need to update frontend to convey this information, which may then expose a natural solution to the situation.
It's fairly simple to create a request queue and execute them serially, but it seems potentially fraught with new challenges.
E.g. If a user clicks 5 checkboxes, and order matters, a failed execution of the second update would mean we would need to stop any further execution of boxes 3 through 5 until update 2 could be completed. We'll also need to figure out how we'll handle timeouts, retries, and backoff. There is some complexity as to how we want to convey all this to the end user.
Let's say we're completely set on going that route, however. In that case, your use of Redux for state management isn't terribly important, nor is the library you use for sending your requests.
As you suggested, we'll just create an in-memory queue of updates to be made and dequeue serially. Each time a user makes an update to a box, we'll push to that queue and attempt to send updates. Our processEvents function will retain state as to whether a request is in motion or not, which it will use to decide whether to take action or not.
Each time a user clicks a box, the event is added to the queue, and we attempt processing. If processing is already ongoing or we have no events to process, we don't take any action. Each time a processing round finishes, we check for further events to process. You'll likely want to hook into this cycle with Redux and fire new actions to indicate event success and update the state and UI for each event processed and so on. It's possible one of the libraries you use offer some feature like this as well.
// Get a better Queue implementation if queue size may get high.
class Queue {
_store = [];
enqueue = (task) => this._store.push(task);
dequeue = () => this._store.shift();
length = () => this._store.length;
}
export const createSerialProcessor = (asyncProcessingCallback) => {
const updateQueue = new Queue();
const addEvent = (params, callback) => {
updateQueue.enqueue([params, callback]);
};
const processEvents = (() => {
let isReady = true;
return async () => {
if (isReady && updateQueue.length() > 0) {
const [params, callback] = updateQueue.dequeue();
isReady = false;
await asyncProcessingCallback(params, callback); // retries and all that include
isReady = true;
processEvents();
}
};
})();
return {
process: (params, callback) => {
addEvent(params, callback);
processEvents();
}
};
};
Hope this helps.
Edit: I just noticed you included a codesandbox, which is very helpful. I've created a copy of your sandbox with updates made to achieve your end and integrate it with your Redux setup. There are some obvious shortcuts still being taken, like the Queue class, but it should be about what you're looking for: https://codesandbox.io/s/dank-feather-hqtf7?file=/src/lib/createSerialProcessor.js
In case you would like to use redux-saga, you can use the actionChannel effect in combination with the blocking call effect to achieve your goal:
Working fork:
https://codesandbox.io/s/hoh8n
Here is the code for boxSagas.js:
import {actionChannel, call, delay, put, take} from 'redux-saga/effects';
// import axios from 'axios';
import {submitSuccess, submitFailure} from '../actions/boxActions';
import {SUBMIT_REQUEST} from '../types/boxTypes';
function* requestSaga(action) {
try {
// const result = yield axios.get(`https://jsonplaceholder.typicode.com/todos`);
yield delay(action.payload.timeout);
yield put(submitSuccess(action.payload.id));
} catch (error) {
yield put(submitFailure());
}
}
export default function* boxSaga() {
const requestChannel = yield actionChannel(SUBMIT_REQUEST); // buffers incoming requests
while (true) {
const action = yield take(requestChannel); // takes a request from queue or waits for one to be added
yield call(requestSaga, action); // starts request saga and _waits_ until it is done
}
}
I am using the fact that the box reducer handles the SUBMIT_REQUEST actions immediately (and sets given id as pending), while the actionChannel+call handle them sequentially and so the actions trigger only one http request at a time.
More on action channels here:
https://redux-saga.js.org/docs/advanced/Channels/#using-the-actionchannel-effect
Just store the promise from a previous request and wait for it to resolve before initiating the next request. The example below uses a global variable for simplicity - but you can use smth else to preserve state across requests (e.g. extraArgument from thunk middleware).
// boxActions.ts
let submitCall = Promise.resolve();
export const submit = (id, timeout) => async (dispatch) => {
dispatch(submitRequest(id));
submitCall = submitCall.then(() => axios.get(`https://jsonplaceholder.typicode.com/todos`))
try {
await submitCall;
setTimeout(() => {
return dispatch(submitSuccess(id));
}, timeout);
} catch (error) {
return dispatch(submitFailure());
}
};
we have a project, with a nextjs, redux saga, typescript setup.
SSR is very important for our web app, but we also have a lot of relevant widgets on every page.
This is why our page is structured in a modular way, where every widget (1-2 per page) loads the data it needs.
These widgets are relevant for SEO reasons now.
My problem is, that the API requests are not made on the serverside though. Right now it only returns the defaultState of every widget on the server and only loads them on the client.
I have searched and found a lot of instructions on how to do it, but they all rely on the fact that nextjs waits for the "getInitialProps" method until it returns the result from the server.
Since that lifecycle method is only available in the "pages" folder, that doesn't work for me.
Also if I block the "getInitialProps" component, the component is never really rendered.
Our pages are structured like below:
- pages/Home
- <HomeContainer ...> (fetches data for the main page)
- <HomeComponent >
- <Widget1Container ...> (fetches data)
- <Widget2Container ...> (fetches data)
What I want is for the serverside to wait for all the requests provided, before it returns the page to the user.
Because of the complex nature of different widgets on a page, it is not possible to create "combined" endpoint where we get the data in the "pages/Home" folder.
I know it's not ideal, but how could we make sure that the server actually makes all 3 requests (homecontainer, widget1container, widget2container) and awaits there responses, before returning?
I would like to have it like angular-universal does it. Just wait until there are not open requests or promises and then just render.
Any ideas?
Any help or ideas are deeply appreciated.
thanks
since you are redux sage, in getServerSideProps, send the start signal to saga
export const getServerSideProps = wrapper.getServerSideProps(
async (context) => {
store.dispatch(
fetchWidgetsStart("add payload")
);
store.dispatch(END);
await (store as SagaStore).sagaTask.toPromise();
const state = store.getState();
// I am making up the reducer name
const widgetListState = state.widgetsList ? state.widgetList : null;
return { props: { productListState } };
}
);
inside saga function:
export function* fetchWidgetsStart() {
yield takeLatest(
WidgetListActionTypes.WIDGET_LIST_START,
fetchWidgetssAsync
);
}
function* fetchWidgetsAsync(action: IFetchWidgetssStart) {
try {
const res: AxiosResponse<IWidget[] | []> = Promise.all([
yield axios.get(fetchWidget1),
yield axios.get(fetchWidget2),
yield axios.get(fetchWidget3)
])
yield put(fetchWidgetSuccess(res));
} catch (e: any) {
yield put(
fetchWidgetFailure(
e.response && e.response.data.detail
? e.response.data.detail
: e.message
)
);
}
}
The Promise.all() method takes an iterable of promises as an input, and returns a single Promise that resolves to an array of the results of the input promises. This returned promise will resolve when all of the input's promises have resolved, or if the input iterable contains no promises. It rejects immediately upon any of the input promises rejecting or non-promises throwing an error, and will reject with this first rejection message / error.
I am developing a react js application and we are using a promise based library axios for calling APIs.
Now, in the initial part of application, user gets a login page, when the login is successful, we contact different systems to retrieve some extra information about user.
axios
.get('url to authentication endpoint') // 1st call
.then(response => {
// if login is successful
// 1. retrieve the user preferences like, on the customised screens what fields user wanted to see
axios.get('user preference endpoint') // 2nd call
// 2. send a request to one more external systems, which calculates what user can see and not based on LDAP role
axios.get('role calculation endpoint') // 3rd call
})
.catch(error => {
})
Now I can see that I can use
axios.all()
for second and third call, but with promised based client, how to chain first and second call? To retrieve user preferences, I have to wait for user to be authenticated.
How to chain this calls in a promise based way, rather than callback style?
as mentioned in the thread for this Github issue, axios() and axios.all() return Promise objects which can be chained however you see fit:
axios.get('/auth')
.then(function(response) {
return axios.all([ axios.get('/preferences'), axios.get('/roles') ]);
})
.then(function(responses) {
const [
preferencesResponse,
rolesResponse
] = responses;
// do more things
})
.catch(function(error) {
console.log(error);
});
Dan O's answer is very good and it works perfectly but it's much readable using async/await although it's also working with promises under the hoood
async yourReactClassFunction(){
try{
let getAuth = await axios.get('/auth');
//if login not successful return;
let result = await Promise.all([axios.get('/preferences'), axios.get('/roles')]);
//Do whatever with the results.
}catch(e){
//TODO error handling
}
}
Although it's the same thing, 'feels' more readable in my very subjective opinion
THE SHORT QUESTION
What is best practice for executing a function after an action has been triggered?
I'm tempted to detect the action in middleware. If its the action I want, execute the function and then pass the action to the reducers. But this seems a bit hacky?
FYI The function uses jQuery to get all codemirror elements on the page and clears the history of all of them
THE FULL QUESTION WITH BACKGROUND (for those who want more depth)
The code I am working with (not my code) fires an action (FAKE_ACTION)
The payload of FAKE_ACTION is another action (REAL_ACTION).
FAKE_ACTION stores REAL_ACTION in a list in global redux state.
Later on, REAL_ACTION will be pulled from the list and triggered.
After REAL_ACTION is triggered I want to execute a function. Where should this function live?
It looks like you are wondering how to handle async operations (side effects) in a redux context. This happens indeed via a middleware but you don't have to write your own. There are projects like redux-thunk or better redux-saga. It is basically logic that stands between the dispatch and the reducers.
you have to perform asynchronus operation for this.
you can use either middleware like redux-thunk or either you can go for javascript promise or any other callback asynchronus function.
You can define a function. In your actions, post an Ajax call when you dispatch a response you will be calling either a success callback or an error callback. So in dispatch, you can define your function.
export function loginUser(email, password) {
return (dispatch, getState) => {
dispatch(loginRequest())
return ajax.post(URL_PREFIX+"/auth/login", { email, password })
.then(res => {
// console.log("path: ", path)
dispatch(loginSuccess(res))
const value = loginUtils.getCurrentUser() //function that you are talking about
})
.catch(errors => {
dispatch(loginFail(errors));
})
}
}
and in loginUtils the function getCurrentUser() is like below
export function getCurrentUser() {
return 'test'
}
Some useful answers here but none are quite what I'm after (given the messy codebase I'm working with) / didn't feel right.
I ended up using store.subscribe which allows you to subscribe to changes in the store's data and react accordingly.
See https://redux.js.org/docs/api/Store.html#subscribe for more info
I am trying to understand a bit more about flux architecture and am designing a simple login component. Suppose when you login (POST ajax) and an error comes back. How would the information flow in flux?
I think the LoginComponent should do the work on a handleSubmit function.
After the ajax call comes back with an error and message should the component create an action such as "UpdateLoginStatus with payload {message: "no e-mail found"}. This would then trigger a LoginStore or something to save the status message and then it would emit an event such as "LoginStatusMessageChanged".
Another totally different component called LoginStatusMessage would register to listen to events on the LoginStore. It would get notified of this event and then proceede to update it's own state with the message. It would go out to LoginStore and fetch the message and display it to the user via render.
I believe this example covere your question quite well: https://github.com/przeor/react-router-flux-starter-kit/
Don't let your Component do the request. That would be mixing UI with business logic. That is always bad.
Entered username, password and button should be handled by your React component. Whenever the button gets clicked, the component should trigger an action.
//Component.js
handleSubmit: function(){
UserActions.login(this.state.username, this.state.password);
}
The action informs the store:
//UserActions.js
login(username, password) {
this.dispatch({username: username, password: password});
}
The Store then executes the needed AJAX (as the actions never should cause changes or requests themselves).
Depending on success or error, your store then triggers a new action. It should never save the response directly. Never save data in stores without an preceding action. If you want to, you can save that you are currently logging in (For instance, if you want to animate a spinner)
//UserStore.js
handleLogin(credentials) {
this.isLoggingIn = true;
AuthService.login(credentials.username, credentials.password).then((user) => {
UserActions.loginSuccess(user);
}).catch((errorMessage) => {
UserActions.loginFailed(errorMessage);
})
}
The actions again do their dumb thing, as they always should be nothing but dumb messengers.
//UserActions.js
loginSuccess(user) {
this.dispatch(user);
}
loginFailed(errorMessage) {
this.dispatch(errorMessage);
}
Up next: handle the actions in your store!
//UserStore.js
handleLoginSuccess(user) {
this.user = user;
this.isLoggedIn = true;
this.isLoggingIn = false;
}
handleLoginFailed(errorMessage) {
this.errorMessage = errorMessage;
this.isLoggingIn = false;
}
That's it. As long as your component listens to the UserStore, it get's informed when your user logs in, is successfully logged in or had an error, while having a clear separation of Logic and UI and a unimanual flow.
EDIT: Code examples are mostly pseudocodish. Actual implementation depend on your Flux framework.