Properly handling state and WebSockets in a functional way - javascript

This is part of a larger project, where I am rewriting the imperative code of my multiplayer game to be functional. Right now, I am more or less at the start, trying to think at a high level about the structure. Here is what I had in mind:
The part that bridges the imperative-functional gap is this class:
class MessageStream<T> {
private readonly actions: T[] = []
private popResolvers: ((value: T) => void)[] = []
push(value: T) {
this.actions.push(value)
if (this.popResolvers.length > 0) {
this.popResolvers.shift()(this.actions.shift())
}
}
pop(): Promise<T> {
return new Promise<T>((resolve) => {
if (this.actions.length > 0 && this.popResolvers.length === 0) {
resolve(this.actions.shift())
} else {
this.popResolvers.push(resolve)
}
})
}
}
I start the web server like this:
const server = new WebServer.Server({ port: 3000 })
const messageStream = new MessageStream<string>()
const initialState: State = { letters: "a" }
server.on("connection", (socket) => {
socket.on("message", (data) => {
try {
messageStream.push(data.toString())
} catch (error) {
console.log("[Error index.ts]", error)
}
})
})
This is where the state updating logic would go:
const handle = async (stream: MessageStream<string>, state: State) => {
const action = await stream.pop()
// do some other thinking to update the state, such as:
handle(stream, { letters: state.letters + action })
}
And this is how we link them:
handle(messageStream, initialState)
Is this the right way to go about writing an FRP-driven server for a multiplayer game? Is the "architecture" here correct? Is there a way to avoid MessageStream, or is that the right way to do this? The recursion in handle seems a little suspicious (feels like the stack can get huge), but I don't see any other way to avoid a global state.
The other confusing part here is that this structure forces all operations to be processed in sequence. Nothing can happen concurrently. But what if processing a message was an intensive operation that I wanted to parallelize, and I only wanted to synchronize updating the state? How, structurally, would one go about doing that?
Edit
Here is a rewrite of handle to be more like reduce:
const reduce = async (
stream: MessageStream<Action>,
callbackfn: (
state: State,
action: Action
) => { newState: State; sideEffects: SideEffect[] },
initialState: State,
executeSideEffect: (sideEffect: SideEffect) => void
) => {
let state = initialState
while (true) {
const action = await stream.pop()
const { newState, sideEffects } = callbackfn(state, action)
sideEffects.map(executeSideEffect)
state = newState
}
}
// example usage
reduce(
messageStream,
(s, a) => ({
newState: { letters: s.letters + a.addLetter },
sideEffects: [
{
type: SideEffectType.NotifyUser,
info: `Got your message, ${a.addLetter}!`,
},
],
}),
{ letters: "a" },
(e) => console.log(e)
)
Then, in socket.on("message"), we push the action to messageStream like above. This way, all the non-pure behavior is contained to the top level of the reduce function. However, one potential anti-pattern is that reduce never returns anything. Is that bad?

Related

Best practise to combine multiple rest calls to populate 1 graphQL type in apollo-server

I have graphql User type that needs information from multiple REST api's and different servers.
Basic example: get the user firstname from rest domain 1 and get lastname from rest domain 2. Both rest domain have a common "userID" attribute.
A simplefied example of my resolver code atm:
user: async (_source, args, { dataSources }) => {
try {
const datasource1 = await dataSources.RESTAPI1.getUser(args.id);
const datasource2 = await dataSources.RESTAPI2.getUser(args.id);
return { ...datasource1, ...datasource2 };
} catch (error) {
console.log("An error occurred.", error);
}
return [];
}
This works fine for this simplefied version, but I have 2 problems with this solution:
first, IRL there is a lot of logic going into merging the 2 json results. Since some field are shared but have different data (or are empty). So it's like cherry picking both results to create a combined result.
My second problem is that this is still a waterfall method. First get the data from restapi1, when thats done call restapi2. Basicly apollo-server is reintroducing rest-waterfall-fetch graphql tries to solve.
Keeping these 2 problems in mind.. Can I optimise this piece of code or rewrite is for better performance or readability? Or are there any packages that might help with this behavior?
Many thanks!
With regard to performance, if the two calls are independent of one another, you can utilize Promise.all to execute them in parallel:
const [dataSource1,dataSource2] = await Promise.all([
dataSources.RESTAPI1.getUser(args.id),
dataSources.RESTAPI2.getUser(args.id),
])
We normally let GraphQL's default resolver logic do the heavy lifting, but if you're finding that you need to "cherry pick" the data from both calls, you can return something like this in your root resolver:
return { dataSource1, dataSource2 }
and then write resolvers for each field:
const resolvers = {
User: {
someField: ({ dataSource1, dataSource2 }) => {
return dataSource1.a || dataSource2.b
},
someOtherField: ({ dataSource1, dataSource2 }) => {
return someCondition ? dataSource1.foo : dataSource2.bar
},
}
}
Assuming your user resolver returns type User forsake...
type User {
id: ID!
datasource1: RandomType
datasource1: RandomType
}
You can create individual resolvers for each field in type User, this can reduce the complexity of the user Query, to only the requested fields.
query {
user {
id
datasource1 {
...
}
}
}
const resolvers = {
Query: {
user: () => {
return { id: "..." };
}
},
User: {
datasource1: () => { ... },
datasource2: () => { ... } // i wont execute
}
};
datasource1 & datasource2 resolvers will only execute in parallel, after Query.user executes.
For parallel call.
const users = async (_source, args, { dataSources }) => {
try {
const promises = [
dataSources.RESTAPI1,
dataSources.RESTAPI2
].map(({ getUser }) => getUser(args.id));
const data = await Promise.all(promises);
return Object.assign({}, ...data);
} catch (error) {
console.log("An error occurred.", error);
}
return [];
};

RxJS: Batch requests and share response

Let's imagine i have a function fetchUser which takes as parameter userId and return an observable of user.
As i am calling this method often, i want to batch the ids to perform one request with multiple ids instead !
Here my troubles began...
I can't find a solution to do that without sharing an observable between the different calls of fetchUser.
import { Subject, from } from "rxjs"
import { bufferTime, mergeMap, map, toArray, filter, take, share } from "rxjs/operators"
const functionThatSimulateAFetch = (userIds: string[]) => from(userIds).pipe(
map((userId) => ({ id: userId, name: "George" })),
toArray(),
)
const userToFetch$ = new Subject<string>()
const fetchedUser$ = userToFetch$.pipe(
bufferTime(1000),
mergeMap((userIds) => functionThatSimulateAFetch(userIds)),
share(),
)
const fetchUser = (userId: string) => {
const observable = fetchedUser$.pipe(
map((users) => users.find((user) => user.id === userId)),
filter((user) => !!user),
take(1),
)
userToFetch$.next(userId)
return observable
}
But that's ugly and it has multiple troubles:
If i unsubscribe from the observable returned by fetchUser before the timer of bufferTime is finished, it doesn't prevent the fetch of the user.
If i unsubscribe from all the observables returned by fetchUser before the fetch of the batch is finished, it doesn't cancel the request.
Error handling is more complex
etc
More generally: i don't know how to solve the problems requiring sharing resources using RxJS. It's difficult to find advanced example of RxJS.
I think #Biggy is right.
This is the way I understand the problem and what you want to achieve
There are different places in your app where you want to fetch users
You do not want to fire a fetch request all the time, rather you
want to buffer them and send them at a certain interval of time,
let's say 1 second
You want to cancel a certain buffer and avoid that for that 1 second
interval a request to fetch a batch of users is fired
At the same time, if somebody, let's call it Code at Position
X has asked for a User and just few milliseconds later somebody
else, i.e. Code at Position Y cancels the entire batch of
requests, then Code at Position X has to receive some sort of
answer, let's say a null
More, you may want to be able to ask to fetch a User and then change
your mind, if within the interval of the buffer time, and and avoid
this User to be fetched (I am far from sure this is really something
you want, but it seems somehow to emerge from your question
If this is all true, then you probably have to have some sort of queuing mechanism, as Buggy suggested.
Then there may be many implementations of such mechanism.
What you have is a good, but as with everything RxJS, but the devil is in the details.
Issues
The switchMaping
mergeMap((userIds) => functionThatSimulateAFetch(userIds)),
This is where you first go wrong. By using a merge map here, you are making it impossible to tell appart the "stream of requests" from the "stream returned by a single request":
You are making it near impossible to unsubscribe from an individual request (to cancel it)
You are making it impossible to handle errors
It falls appart if your inner observable emits more than once.
Rather, what you want is to emit individual BatchEvents, via a normal map (producing an observable of observable), and switchMap/mergeMap those after the filtering.
Side effects when creating an observable & Emitting before subscribing
userToFetch$.next(userId)
return observable
Don’t do this. An observable by itself does not actually do anything. It’s a "blueprint" for a sequence of actions to happen when you subscribe to it. By doing this, you’ll only create a batch action on observable creating, but you’re screwed if you get multiple or delayed subscriptions.
Rather, you want to create an observable from defer that emits to userToFetch$ on every subscription.
Even then you’ll want to subscribe to your observable before emitting to userToFetch: If you aren’t subscribed, your observable is not listening to the subject, and the event will be lost. You can do this in a defer-like observable.
Solution
Short, and not very different from your code, but structure it like this.
const BUFFER_TIME = 1000;
type BatchEvent = { keys: Set<string>, values: Observable<Users> };
/** The incoming keys */
const keySubject = new Subject<string>();
const requests: Observable<{ keys: Set<string>, values: Observable<Users> }> =
this.keySubject.asObservable().pipe(
bufferTime(BUFFER_TIME),
map(keys => this.fetchBatch(keys)),
share(),
);
/** Returns a single User from an ID. Batches the request */
function get(userId: string): Observable<User> {
console.log("Creating observable for:", userId);
// The money observable. See "defer":
// triggers a new subject event on subscription
const observable = new Observable<BatchEvent>(observer => {
this.requests.subscribe(observer);
// Emit *after* the subscription
this.keySubject.next(userId);
});
return observable.pipe(
first(v => v.keys.has(userId)),
// There is only 1 item, so any *Map will do here
switchMap(v => v.values),
map(v => v[userId]),
);
}
function fetchBatch(args: string[]): BatchEvent {
const keys = new Set(args); // Do not batch duplicates
const values = this.userService.get(Array.from(keys)).pipe(
share(),
);
return { keys, values };
}
This does exactly what you were asking, including:
Errors are propagated to the recipients of the batch call, but nobody else
If everybody unsubscribes from a batch, the observable is canceled
If everybody unsubscribes from a batch before the request is even fired, it never fires
The observable behaves like HttpClient: subscribing to the observable fires a new (batched) request for data. Callers are free to pipe shareReplay or whatever though. So no surprises there.
Here is a working stackblitz Angular demo: https://stackblitz.com/edit/angular-rxjs-batch-request
In particular, notice the behavior when you "toggle" the display: You’ll notice that re-subscribing to existing observables will fire new batch requests, and that those requests will cancel (or outright not fire) if you re-toggle fast enough.
Use case
In our project, we use this for Angular Tables, where each row needs to individually fetch additional data to render. This allows us to:
chunk all the requests for a "single page", without needing any special knowledge of pagination
Potentially fetch multiple pages at once if the user paginates fast
re-use existing results even if page size changes
Limitations
I would not add chunking or rate limitting into this. Because the source observable is a dumb bufferTime you run into issues:
The "chunking" will happen before the deduping. So if you have 100 requests for a single userId, you’ll end up firing several requests with only 1 element
If you rate limit, you’ll not be able to inspect your queue. So you may end up with a very long queue containing multiple same requests.
This is a pessimistic point of view though. Fixing it would mean going full out with a stateful queue/batch mechanism, which is an order of magnitude more complex.
I'm not sure if this is the best way to solve this problem (at least it need tests), but I will try to explain my point of view.
We have 2 queue: for pending and for feature requests.
result to help delivery response/error to subscribers.
Some kind of worker who is based on some schedule takes a task from the queue to do the request.
If i unsubscribe from the observable returned by fetchUser before the
timer of bufferTime is finished, it doesn't prevent the fetch of the
user.
Unsubscribe from fetchUser will cleanup the request queue and worker will do nothing.
If i unsubscribe from all the observables returned by fetchUser before
the fetch of the batch is finished, it doesn't cancel the request.
Worker subscribe until isNothingRemain$
const functionThatSimulateAFetch = (userIds: string[]) => from(userIds).pipe(
map((userId) => ({ id: userId, name: "George" })),
toArray(),
tap(() => console.log('API_CALL', userIds)),
delay(200),
)
class Queue {
queue$ = new BehaviorSubject(new Map());
private get currentQueue() {
return new Map(this.queue$.getValue());
}
add(...ids) {
const newMap = ids.reduce((acc, id) => {
acc.set(id, (acc.get(id) || 0) + 1);
return acc;
}, this.currentQueue);
this.queue$.next(newMap);
};
addMap(idmap: Map<any, any>) {
const newMap = (Array.from(idmap.keys()))
.reduce((acc, id) => {
acc.set(id, (acc.get(id) || 0) + idmap.get(id));
return acc;
}, this.currentQueue);
this.queue$.next(newMap);
}
remove(...ids) {
const newMap = ids.reduce((acc, id) => {
acc.get(id) > 1 ? acc.set(id, acc.get(id) - 1) : acc.delete(id);
return acc;
}, this.currentQueue)
this.queue$.next(newMap);
};
removeMap(idmap: Map<any, any>) {
const newMap = (Array.from(idmap.keys()))
.reduce((acc, id) => {
acc.get(id) > idmap.get(id) ? acc.set(id, acc.get(id) - idmap.get(id)) : acc.delete(id);
return acc;
}, this.currentQueue)
this.queue$.next(newMap);
};
has(id) {
return this.queue$.getValue().has(id);
}
asObservable() {
return this.queue$.asObservable();
}
}
class Result {
result$ = new BehaviorSubject({ ids: new Map(), isError: null, value: null });
select(id) {
return this.result$.pipe(
filter(({ ids }) => ids.has(id)),
switchMap(({ isError, value }) => isError ? throwError(value) : of(value.find(x => x.id === id)))
)
}
add({ isError, value, ids }) {
this.result$.next({ ids, isError, value });
}
clear(){
this.result$.next({ ids: new Map(), isError: null, value: null });
}
}
const result = new Result();
const queueToSend = new Queue();
const queuePending = new Queue();
const doRequest = new Subject();
const fetchUser = (id: string) => {
return Observable.create(observer => {
queueToSend.add(id);
doRequest.next();
const subscription = result
.select(id)
.pipe(take(1))
.subscribe(observer);
// cleanup queue after got response or unsubscribe
return () => {
(queueToSend.has(id) ? queueToSend : queuePending).remove(id);
subscription.unsubscribe();
}
})
}
// some kind of worker that take task from queue and send requests
doRequest.asObservable().pipe(
auditTime(1000),
// clear outdated results
tap(()=>result.clear()),
withLatestFrom(queueToSend.asObservable()),
map(([_, queue]) => queue),
filter(ids => !!ids.size),
mergeMap(ids => {
// abort the request if it have no subscribers
const isNothingRemain$ = combineLatest(queueToSend.asObservable(), queuePending.asObservable()).pipe(
map(([queueToSendIds, queuePendingIds]) => Array.from(ids.keys()).some(k => queueToSendIds.has(k) || queuePendingIds.has(k))),
filter(hasSameKey => !hasSameKey)
)
// prevent to request the same ids if previous requst is not complete
queueToSend.removeMap(ids);
queuePending.addMap(ids);
return functionThatSimulateAFetch(Array.from(ids.keys())).pipe(
map(res => ({ isErorr: false, value: res, ids })),
takeUntil(isNothingRemain$),
catchError(error => of({ isError: true, value: error, ids }))
)
}),
).subscribe(res => result.add(res))
fetchUser('1').subscribe(console.log);
const subs = fetchUser('2').subscribe(console.log);
subs.unsubscribe();
fetchUser('3').subscribe(console.log);
setTimeout(() => {
const subs1 = fetchUser('10').subscribe(console.log);
subs1.unsubscribe();
const subs2 = fetchUser('11').subscribe(console.log);
subs2.unsubscribe();
}, 2000)
setTimeout(() => {
const subs1 = fetchUser('20').subscribe(console.log);
subs1.unsubscribe();
const subs21 = fetchUser('20').subscribe(console.log);
const subs22 = fetchUser('20').subscribe(console.log);
}, 4000)
// API_CALL
// ["1", "3"]
// {id: "1", name: "George"}
// {id: "3", name: "George"}
// API_CALL
// ["20"]
// {id: "20", name: "George"}
// {id: "20", name: "George"}
stackblitz example
FYI, i tried to create a generic batched task queue using the answers of
#buggy & #picci :
import { Observable, Subject, BehaviorSubject, from, timer } from "rxjs"
import { catchError, share, mergeMap, map, filter, takeUntil, take, bufferTime, timeout, concatMap } from "rxjs/operators"
export interface Task<TInput> {
uid: number
input: TInput
}
interface ErroredTask<TInput> extends Task<TInput> {
error: any
}
interface SucceededTask<TInput, TOutput> extends Task<TInput> {
output: TOutput
}
export type FinishedTask<TInput, TOutput> = ErroredTask<TInput> | SucceededTask<TInput, TOutput>
const taskErrored = <TInput, TOutput>(
taskFinished: FinishedTask<TInput, TOutput>,
): taskFinished is ErroredTask<TInput> => !!(taskFinished as ErroredTask<TInput>).error
type BatchedWorker<TInput, TOutput> = (tasks: Array<Task<TInput>>) => Observable<FinishedTask<TInput, TOutput>>
export const createSimpleBatchedWorker = <TInput, TOutput>(
work: (inputs: TInput[]) => Observable<TOutput[]>,
workTimeout: number,
): BatchedWorker<TInput, TOutput> => (
tasks: Array<Task<TInput>>,
) => work(
tasks.map((task) => task.input),
).pipe(
mergeMap((outputs) => from(tasks.map((task, index) => ({
...task,
output: outputs[index],
})))),
timeout(workTimeout),
catchError((error) => from(tasks.map((task) => ({
...task,
error,
})))),
)
export const createBatchedTaskQueue = <TInput, TOutput>(
worker: BatchedWorker<TInput, TOutput>,
concurrencyLimit: number = 1,
batchTimeout: number = 0,
maxBatchSize: number = Number.POSITIVE_INFINITY,
) => {
const taskSubject = new Subject<Task<TInput>>()
const cancelTaskSubject = new BehaviorSubject<Set<number>>(new Set())
const cancelTask = (task: Task<TInput>) => {
const cancelledUids = cancelTaskSubject.getValue()
const newCancelledUids = new Set(cancelledUids)
newCancelledUids.add(task.uid)
cancelTaskSubject.next(newCancelledUids)
}
const output$: Observable<FinishedTask<TInput, TOutput>> = taskSubject.pipe(
bufferTime(batchTimeout, undefined, maxBatchSize),
map((tasks) => {
const cancelledUids = cancelTaskSubject.getValue()
return tasks.filter((task) => !cancelledUids.has(task.uid))
}),
filter((tasks) => tasks.length > 0),
mergeMap(
(tasks) => worker(tasks).pipe(
takeUntil(cancelTaskSubject.pipe(
filter((uids) => {
for (const task of tasks) {
if (!uids.has(task.uid)) {
return false
}
}
return true
}),
)),
),
undefined,
concurrencyLimit,
),
share(),
)
let nextUid = 0
return (input$: Observable<TInput>): Observable<TOutput> => input$.pipe(
concatMap((input) => new Observable<TOutput>((observer) => {
const task = {
uid: nextUid++,
input,
}
const subscription = output$.pipe(
filter((taskFinished) => taskFinished.uid === task.uid),
take(1),
map((taskFinished) => {
if (taskErrored(taskFinished)) {
throw taskFinished.error
}
return taskFinished.output
}),
).subscribe(observer)
subscription.add(
timer(0).subscribe(() => taskSubject.next(task)),
)
return () => {
subscription.unsubscribe()
cancelTask(task)
}
})),
)
}
With our example:
import { from } from "rxjs"
import { map, toArray } from "rxjs/operators"
import { createBatchedTaskQueue, createSimpleBatchedWorker } from "mmr/components/rxjs/batched-task-queue"
const functionThatSimulateAFetch = (userIds: string[]) => from(userIds).pipe(
map((userId) => ({ id: userId, name: "George" })),
toArray(),
)
const userFetchQueue = createBatchedTaskQueue(
createSimpleBatchedWorker(
functionThatSimulateAFetch,
10000,
),
)
const fetchUser = (userId: string) => {
return from(userId).pipe(
userFetchQueue,
)
}
I am open to any improvement suggestions

Really need some help figuring out the logic of componentWillMount() prior to render

this might be kind of long read, I've read and tried so many solutions without success! Essentially what I have is three MySQL tables, one with a list of users, and one with a list of file data. They are paired with a third table, which has a column for user id and a column for file id.
When a user logs into the app, it grabs their ID from Table 1, goes to Table 3, finds all the file IDs that are in the same row as their user ID, and then returns the file information from Table 2. Mostly straight forward, except it's not.
My current code:
componentWillMount() {
this.getClientFiles();
}
Which calls:
getClientFiles() {
let id = this.props.currentUser.user_id;
let file_refs = [];
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.setState({
file_refs
});
this.getFileData();
});
}
My understanding of this is that this.getFileData(); should ONLY run once the axios GET request is successful (because of .then). The file refs are all returned, and the added to an array and put in state for the duration of the client's session.
Then this should run:
getFileData() {
let fileRefs = this.state.file_refs;
let fileData = [];
for (let i = 0; i < fileRefs.length; i++) {
axios
.get("/files/get-file/" + fileRefs[i])
.then(res => {
fileData.push(res.data.response);
this.setState({
client_files: fileData,
returned_data: true
});
})
.catch(err => console.log(err.response.data));
}
}
Here, the function cycles through the fileRefs in state, makes a call for each reference ID, and returns that to fileData and saves it to state.
The problem.... on first page load after a login, the files do not render. If you hit cmd+R to refresh, boom there they are. I understand the chain of promises, and the async nature of JS functions, I understand that componentWillMount() should run prior to the mounting of the component, and that setState should trigger a re-render of a component.
Things I've tried:
1) Adding the following code in after render() prior to return( :
if (this.state.returned_data === false) {
this.getClientFiles();
}
The result is a flickering of renders, 4-5 of them, as the functions run async before the state of returned_data is set to true.
2) Moving the setState({ returned_data: true }) into the getClientFiles() function. This just ends the render early, resulting in no files until the page is refreshed.
3) Swapping out componentWillMount() for componentDidMount().
Clearly, there is a fundamental aspect of the chain of functions and React's built in methods that I'm missing.
Can anybody help?
EDIT #1
The issue seems to be that on first render, let id = this.props.currentUser.user_id; is undefined, so the call in getClientFiles is actually going to /users/get-client-files/undefined
EDIT #2 - Requested by #devserkan
I hope this is what you wanted :)
First load
get-client-files/${id}: Returns an empty array
/get-file/" + fileRefs[i]: Doesn't run
Second load:
get-client-files/${id}: Returns array with 5 items
/get-file/" + fileRefs[i]: Runs appropriately 5 times with the details of each file.
So clearly, the issue is with the fact that get-client-files/${id} isn't getting anything because it doesn't have the ${id} to search from. The ID is passed down via props, but doesn't seem to be available immediately.
EDIT #3
Here is the function that gets the ID, and sets it to state.
getUser = () => {
let localToken = localStorage.getItem("iod_tkn");
axios({
url: "/admins/current",
method: "get",
headers: {
Authorization: localToken
}
})
.then(result => {
this.setState({
isLoggedIn: true,
user: result.data,
user_id: result.data.user_id
});
})
.catch(err => {
this.setState({ isLoggedIn: false });
console.log(err);
});
};
And App.js renders the following:
render() {
const { loading } = this.state;
if (loading) {
return <Spinner />;
}
return (
<AdminProvider>
<FileProvider>
<Provider>
<Appbar isLoggedIn={this.state.isLoggedIn} logout={this.logout} />
<Main
getUser={this.getUser}
isLoggedIn={this.state.isLoggedIn}
currentUser={this.state.user}
/>
<BottomNav />
</Provider>
</FileProvider>
</AdminProvider>
);
}
So with passing this.state.user into Main.js, that component should re-render once the props have been received, right?
Since your user_id is coming from an async job, you should do a conditional rendering. Like:
{ user_id && <ClientDashboard user_id={user_id} ... /> }
Also, you can clean up your code a little bit more maybe :) Here I am mimicking your app.
const userFiles = [
{ file_id: 1, client_name: "foo" },
{ file_id: 2, client_name: "bar" },
{ file_id: 3, client_name: "baz" },
];
const files = [
{ file_id: 1, name: "fizz", size: 10 },
{ file_id: 2, name: "buzz", size: 20 },
{ file_id: 3, name: "fuzz", size: 30 },
];
const fakeRequest = () => new Promise( resolve =>
setTimeout( () => resolve(userFiles), 1000)
);
const fakeRequest2 = id => new Promise(resolve => {
const file = files.find( el => id === el.file_id );
setTimeout(() => resolve(file), 1000)
}
);
class App extends React.Component {
state = {
file_refs: [],
client_files: [],
returned_data: false,
}
componentDidMount() {
this.getClientFiles();
}
getClientFiles() {
fakeRequest()
.then(res => {
const file_refs = res.map( el => el.file_id );
this.setState({
file_refs
});
this.getFileData();
});
}
getFileData() {
const {file_refs: fileRefs} = this.state;
const promiseArray = fileRefs.map( id => fakeRequest2( id ) );
Promise.all( promiseArray )
.then( results => this.setState({
client_files: results,
returned_data: true,
}))
}
render() {
const { file_refs, client_files } = this.state;
return (
<div>
{!!file_refs.length && <p>File_refs: {JSON.stringify(file_refs)}</p>}
{!!client_files.length && <p>Client files: {JSON.stringify(client_files)}</p>}
</div>
);
}
}
ReactDOM.render(<App />, document.getElementById("root"));
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react.min.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/15.1.0/react-dom.min.js"></script>
<div id="root"></div>
I don't like for loops :)
The problem is that in componentWillMount() an async call might not retrieve the results on time before the render of the mount phase happens, so you will have unexpected side effects. Most probably the component will render with empty data.
The best place to render data from an async call is componentDidMount().
As a side note, from 16.3 version on, componentWillMount() is considered an unsafe method of the lifecycle, and in future versions will be removed, so you better not use it anymore.
I think there's an issue with your code structuring. setState is an async function which takes a callback as a second parameter. You should take its advantage. You can execute a function after setState is finishing and utilize updated state using the second param callback (updater function) like:
this.setState({
file_refs
}, () => {
this.getFileData();
});
EDITED Second option you shouldn't setState file_refs unless you're using it in your render method.
Try this:
axios.get(`/users/get-client-files/${id}`)
.then(res => {
let response = res.data.response;
for (let i = 0; i < response.length; i++) {
file_refs.push(response[i].file_id);
}
this.getFileData(file_refs);
});
getFileData(file_refs) {
let fileRefs = file_refs;
let fileData = [];
// rest of your code
}
Let me know if the issue still persists. Happy to help

State Variable triggers mutation error

I have the following pseudo-code in my store module
const state = {
users: []
}
const actions = {
addUsers: async ({commit, state}, payload) => {
let users = state.users // <-- problem
// fetching new users
for(let i of newUsersThatGotFetched) {
users.push('user1') // <-- really slow
}
commit('setUsers',users)
}
}
const mutations = {
setUsers: (state, { users }) => {
Vue.set(state, 'users', users)
}
}
Now - when I run this code, I get the following error Error: [vuex] Do not mutate vuex store state outside mutation handlers.
When I put strict mode to false - the error is gone - but the process-time is really, really slow - as if the errors still happen but without getting displayed.
The problem seems to be where I commented // <-- problem, because after I change that line to
let users = []
everything runs flawlessly, but I can't have that because I need the data of state.users
The problem is: users.push('user1'), this is the line that mutates the state.
Remove anything that mutates the state (writes or changes it) from actions and move that into a mutation.
addUsers: async ({ commit }, payload) => {
// fetching new users
commit('setUsers', newUsersThatGotFetched)
}
Then add the new users in the mutation.
const mutations = {
setUsers: (state, users) => {
state.users.concat(users);
// or if you have custom logic
users.forEach(user => {
if (whatever) state.users.push(user)
});
}
}
The reason it is slow is related to Strict mode
Strict mode runs a synchronous deep watcher on the state tree for detecting inappropriate mutations, and it can be quite expensive when you make large amount of mutations to the state. Make sure to turn it off in production to avoid the performance cost.
If you want to speed up the mutation, you could do the changes on a new array which would replace the one in the state when ready.
const mutations = {
setUsers: (state, newUsers) => {
state.users = newUsers.reduce((users, user) => {
if (whatever) users.push(user);
return users;
}, state.users.slice()); // here, we start with a copy of the array
}
}

Redux.js and data with relations

Imagine that you develop some react-redux application (with global immuatable tree-state). And some data have some rules-relations in different tree-branches, like SQL relations between tables.
I.e. if you are working on some company's todos list, each todo has relation(many-to-one) with concrete user. And if you add some new user, you should add empty todo list (to other branch in the state). Or delete user means that you should re-assign user's todos to some (default admin) user.
You can hardcode this relation directly to source code. And it is good and works OK.
But imagine that you have got million small relations for data like this. It will be good that some small "automatic" operations/checks (for support/guard relations) performs automatically according to rules.
May be existed some common approach/library/experience to do it via some set of rules: like triggers in SQL:
on add new user => add new empty todos
on user delete => reassign todos to default user
There are two solutions here. I don't think that you should aim to have this kind of functionality in a redux application, so my first example is not quite what you're looking for but I think is more conical. The second example adopts a DB/orm pattern, which may work fine, but is not conical, and requires
These could be trivially added safely with vanilla redux and redux-thunk. Redux thunk basically allows you to dispatch a single action that its self dispatches multiple other actions--so when you trigger CREATE_USER, just do something along the lines of triggering CREATE_EMPTY_TODO, CREATE_USER, and ASSIGN_TODO in the createUser action. For deleting users, REASSIGN_USER_TODOS and then DELETE_USER.
For the examples you provide, here are examples:
function createTodoList(todos = []) {
return dispatch => {
return API.createTodoList(todos)
.then(res => { // res = { id: 15543, todos: [] }
dispatch({ type: 'CREATE_TODO_LIST_SUCCESS', res });
return res;
});
}
}
function createUser (userObj) {
return dispatch => {
dispatch(createTodoList())
.then(todoListObj => {
API.createUser(Object.assign(userObj, { todoLists: [ todoListObj.id ] }))
.then(res => { // res = { id: 234234, name: userObj.name, todoLists: [ 15534 ]}
dispatch({ type: 'CREATE_USER_SUCCESS', payload: res });
return res;
})
})
.catch(err => console.warn('Could not create user because there was an error creating todo list'));
}
}
Deleteing, sans async stuff.
function deleteUser (userID) {
return (dispatch, getState) => {
dispatch({
type: 'REASSIGN_USER_TODOS',
payload: {
fromUser: userID,
toUser: getState().application.defaultReassignUser
});
dispatch({
type: 'DELETE_USER',
payload: { userID }
});
}
}
The problem with this method, as mentioned in the comments, is that a new developer might come onto the project without knowing what actions already exist, and then create their own version of createUser which doesn't know to create todos. While you can never completely take away their ability to write bad code, you can try to be more defensive by making your actions more structured. For example, if your actions look like this:
const createUserAction = {
type: 'CREATE',
domain: 'USERS',
payload: userProperies
}
you can have a reducer structured like this
function createUserTrigger (state, userProperies) {
return {
...state,
todoLists: {
...state.todoLists,
[userProperies.id]: []
}
}
}
const triggers = {
[CREATE]: {
[USERS]: createUserTrigger
}
}
function rootReducer (state = initialState, action) {
const { type, domain, payload } = action;
let result = state;
switch (type) {
case CREATE:
result = {
...state,
[domain]: {
...state[domain],
[payload.id]: payload
}
};
break;
case DELETE:
delete state[domain][payload.id];
result = { ...state };
break;
case UPDATE:
result = {
...state,
[domain]: {
...state[domain],
[payload.id]: _.merge(state[domain][payload.id], payload)
}
}
break;
default:
console.warn('invalid action type');
return state;
}
return triggers[type][domain] ? triggers[type][domain](result, payload) : result;
}
In this case, you're basically forcing all developers to use a very limited possible set of action types. Its very rigid and I don't really recommend it, but I think it does what you're asking.

Categories

Resources