integrate useImmutable

This commit is contained in:
Michael Fatemi 2021-07-15 09:08:59 -04:00
parent 0735d3ed3a
commit a48ad180e0
10 changed files with 104 additions and 124 deletions

7
.env
View File

@ -1,5 +1,2 @@
REACT_APP_API_DOMAIN_LOCAL=http://localhost:5000/ REACT_APP_API_LOCAL=http://localhost:5000/
REACT_APP_API_DOMAIN_DOCN=https://wheelshare-altbackend-2efyw.ondigitalocean.app/ REACT_APP_API_PROD=https://api.wheelshare.app/
REACT_APP_API_DOMAIN_WWW=https://api.wheelshare.app/
REACT_APP_API_DOMAIN=https://api.wheelshare.app/

View File

@ -3,6 +3,7 @@ import { BrowserRouter, Route, Switch } from 'react-router-dom';
import GroupsProvider from '../state/GroupsProvider'; import GroupsProvider from '../state/GroupsProvider';
import NotificationsProvider from '../state/Notifications/NotificationsProvider'; import NotificationsProvider from '../state/Notifications/NotificationsProvider';
import { useMe } from './hooks'; import { useMe } from './hooks';
import useImmutable from './useImmutable';
import WheelShare from './WheelShare'; import WheelShare from './WheelShare';
import WheelShareLoggedOut from './WheelShareLoggedOut'; import WheelShareLoggedOut from './WheelShareLoggedOut';
@ -23,11 +24,20 @@ const style: CSSProperties = {
export default function App() { export default function App() {
const user = useMe(); const user = useMe();
const [imm] = useImmutable({
x: 0,
y: 0,
z: { a: 1, b: 2, c: [0, 1, 2] },
});
return ( return (
<NotificationsProvider> <NotificationsProvider>
<GroupsProvider> <GroupsProvider>
<div style={{ padding: '1rem', maxWidth: '100vw' }}> <div style={{ padding: '1rem', maxWidth: '100vw' }}>
<div style={style}> <div style={style}>
{JSON.stringify(imm)}
{/* Reset button */}
<button onClick={() => imm.z.a++}>Increment</button>
<button onClick={() => imm.z.c.push(imm.z.c.length)}>Push</button>
<BrowserRouter> <BrowserRouter>
<Switch> <Switch>
<Route <Route

View File

@ -1,11 +1,4 @@
import * as immutable from 'immutable'; import { createContext, useCallback, useEffect, useMemo } from 'react';
import {
createContext,
useCallback,
useEffect,
useMemo,
useState,
} from 'react';
import { import {
cancelCarpoolInvite, cancelCarpoolInvite,
getCarpool, getCarpool,
@ -13,33 +6,23 @@ import {
sendCarpoolInvite, sendCarpoolInvite,
} from '../api'; } from '../api';
import { useMe } from '../hooks'; import { useMe } from '../hooks';
import { ICarpool } from '../types';
import UISecondaryBox from '../UI/UISecondaryBox'; import UISecondaryBox from '../UI/UISecondaryBox';
import useImmutable from '../useImmutable';
import CarpoolDetails from './CarpoolDetails'; import CarpoolDetails from './CarpoolDetails';
import InvitationsAndRequests from './InvitationsAndRequests'; import InvitationsAndRequests from './InvitationsAndRequests';
import MemberList from './MemberList'; import MemberList from './MemberList';
class CarpoolState extends immutable.Record({ type CarpoolState = {
id: 0, id: number;
name: '', name: string;
eventId: -1, event: ICarpool['event'];
event: { members: { id: number; name: string }[];
id: -1, invitations: Record<number, ICarpool['invitations'][0]>;
name: '', };
formattedAddress: '',
latitude: 0,
longitude: 0,
placeId: '',
},
members: immutable.List<{ id: number; name: string }>(),
invitations:
immutable.Map<
number,
{ isRequest: boolean; user: { id: number; name: string } }
>(),
}) {}
export const CarpoolContext = createContext({ export const CarpoolContext = createContext({
carpool: new CarpoolState(), carpool: {} as CarpoolState,
sendInvite: (user: { id: number; name: string }) => { sendInvite: (user: { id: number; name: string }) => {
console.error('not implemented: sendInvite'); console.error('not implemented: sendInvite');
}, },
@ -52,36 +35,36 @@ export const CarpoolContext = createContext({
}); });
export default function Carpool({ id }: { id: number }) { export default function Carpool({ id }: { id: number }) {
const [carpool, setCarpool] = useState(new CarpoolState()); const [carpool, setCarpool] = useImmutable<CarpoolState>({
id,
name: '',
event: {} as ICarpool['event'],
members: [],
invitations: {},
});
useEffect(() => { useEffect(() => {
getCarpool(id).then((carpool) => { getCarpool(id).then((carpool) => {
setCarpool( const invitationsMap: Record<number, ICarpool['invitations'][0]> = {};
new CarpoolState({ carpool.invitations.forEach((invite) => {
id: carpool.id, invitationsMap[invite.user.id] = invite;
name: carpool.name, });
eventId: carpool.eventId || carpool.event.id, setCarpool({
event: carpool.event, id: carpool.id,
members: immutable.List(carpool.members), name: carpool.name,
invitations: immutable.Map( event: carpool.event,
carpool.invitations.map((invite) => [invite.user.id, invite]) members: carpool.members,
), invitations: invitationsMap,
}) });
);
}); });
}, [id]); }, [id, setCarpool]);
const sendInvite = useCallback( const sendInvite = useCallback(
(user: { id: number; name: string }) => { (user: { id: number; name: string }) => {
if (carpool) { if (carpool) {
sendCarpoolInvite(id, user.id) sendCarpoolInvite(id, user.id)
.then(() => { .then(() => {
setCarpool((carpool) => carpool.invitations[user.id] = { isRequest: false, user };
carpool.set(
'invitations',
carpool.invitations.set(user.id, { isRequest: false, user })
)
);
}) })
.catch(console.error); .catch(console.error);
} else { } else {
@ -97,22 +80,14 @@ export default function Carpool({ id }: { id: number }) {
(user: { id: number; name: string }) => { (user: { id: number; name: string }) => {
cancelCarpoolInvite(id, user.id) cancelCarpoolInvite(id, user.id)
.then(() => { .then(() => {
setCarpool( delete carpool.invitations[user.id];
(carpool) =>
carpool && {
...carpool,
invitations: carpool.invitations.filter(
(invite) => invite.user.id !== user.id
),
}
);
}) })
.catch(console.error); .catch(console.error);
}, },
[id] [carpool.invitations, id]
); );
const eventId = carpool.eventId; const eventId = carpool.event.id;
const leave = useCallback(() => { const leave = useCallback(() => {
if (eventId) { if (eventId) {

View File

@ -59,9 +59,8 @@ export default function InvitationList() {
const invitedUserIDs = useMemo( const invitedUserIDs = useMemo(
() => () =>
new Set( new Set(
carpool.invitations Object.values(carpool.invitations)
.filter((invitation) => !invitation.isRequest) .filter((invitation) => !invitation.isRequest)
.valueSeq()
.map((invitation) => invitation.user.id) .map((invitation) => invitation.user.id)
), ),
[carpool.invitations] [carpool.invitations]

View File

@ -24,7 +24,7 @@ export default function MemberList() {
const { leave, carpool } = useContext(CarpoolContext); const { leave, carpool } = useContext(CarpoolContext);
const members = carpool.members; const members = carpool.members;
const membersToShow = members.slice(0, shownMembersCount); const membersToShow = members.slice(0, shownMembersCount);
const hiddenMemberCount = members.size - membersToShow.size; const hiddenMemberCount = members.length - membersToShow.length;
const { sendCarpoolRequest, cancelCarpoolRequest } = const { sendCarpoolRequest, cancelCarpoolRequest } =
useContext(NotificationsContext); useContext(NotificationsContext);
@ -54,7 +54,7 @@ export default function MemberList() {
}} }}
> >
<h3 style={{ marginBlockEnd: '0' }}>Members</h3> <h3 style={{ marginBlockEnd: '0' }}>Members</h3>
{members.size > 0 ? ( {members.length > 0 ? (
<div style={{ display: 'flex', flexDirection: 'column' }}> <div style={{ display: 'flex', flexDirection: 'column' }}>
{membersToShow.map((member) => ( {membersToShow.map((member) => (
<MemberRow member={member} key={member.id} /> <MemberRow member={member} key={member.id} />

View File

@ -1,4 +1,3 @@
import * as immutable from 'immutable';
import { useCallback, useEffect, useRef, useState } from 'react'; import { useCallback, useEffect, useRef, useState } from 'react';
import { green, lightgrey } from '../../lib/colors'; import { green, lightgrey } from '../../lib/colors';
import { import {
@ -14,6 +13,7 @@ import UILink from '../UI/UILink';
import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete'; import UIPlacesAutocomplete from '../UI/UIPlacesAutocomplete';
import UISecondaryBox from '../UI/UISecondaryBox'; import UISecondaryBox from '../UI/UISecondaryBox';
import UISecondaryHeader from '../UI/UISecondaryHeader'; import UISecondaryHeader from '../UI/UISecondaryHeader';
import useImmutable from '../useImmutable';
import useThrottle from '../useThrottle'; import useThrottle from '../useThrottle';
import EventCarpools from './EventCarpools'; import EventCarpools from './EventCarpools';
import EventContext from './EventContext'; import EventContext from './EventContext';
@ -49,17 +49,7 @@ export default function Event({
}); });
const me = useMe(); const me = useMe();
const [tentativeInvites, setTentativeInvites] = useState( const [tentativeInvites] = useImmutable<Record<number, boolean>>({});
immutable.Set<number>()
);
const addTentativeInvite = useCallback((userId: number) => {
setTentativeInvites((t) => t.add(userId));
}, []);
const removeTentativeInvite = useCallback((userId: number) => {
setTentativeInvites((t) => t.delete(userId));
}, []);
const refresh = useCallback(() => { const refresh = useCallback(() => {
getEvent(id).then(setEvent); getEvent(id).then(setEvent);
@ -142,8 +132,6 @@ export default function Event({
event, event,
refresh, refresh,
default: false, default: false,
addTentativeInvite,
removeTentativeInvite,
tentativeInvites, tentativeInvites,
signups, signups,
hasCarpool, hasCarpool,

View File

@ -167,7 +167,7 @@ export default function Carpools() {
createCarpool({ createCarpool({
name: me.name + "'s Carpool", name: me.name + "'s Carpool",
eventId: event.id, eventId: event.id,
invitedUserIds: tentativeInvites.toArray(), invitedUserIds: Object.keys(tentativeInvites).map(Number),
}) })
.then(({ id }) => { .then(({ id }) => {
setCreatedCarpoolId(id); setCreatedCarpoolId(id);
@ -180,12 +180,11 @@ export default function Carpools() {
const tentativeInviteNames = useMemo(() => { const tentativeInviteNames = useMemo(() => {
if (!signups) return []; if (!signups) return [];
const names = tentativeInvites.map((id) => { const names = Object.keys(tentativeInvites).map((id) => {
const signup = signups[id]; const signup = signups[id];
return signup?.user.name; return signup?.user.name;
}); });
const nonNull = names.filter((n) => n != null); return names.filter((n) => n != null);
return nonNull.toArray() as string[];
}, [tentativeInvites, signups]); }, [tentativeInvites, signups]);
let createCarpoolSection; let createCarpoolSection;

View File

@ -1,6 +1,5 @@
import { createContext } from 'react'; import { createContext } from 'react';
import { IEvent, IEventSignup } from '../types'; import { IEvent, IEventSignup } from '../types';
import * as immutable from 'immutable';
const EventContext = createContext({ const EventContext = createContext({
refresh: () => { refresh: () => {
@ -9,13 +8,7 @@ const EventContext = createContext({
event: null! as IEvent, event: null! as IEvent,
default: true, default: true,
signups: {} as Record<string, IEventSignup>, signups: {} as Record<string, IEventSignup>,
addTentativeInvite: (id: number) => { tentativeInvites: {} as Record<number, boolean>,
console.error('not implemented: addTentativeInvite');
},
removeTentativeInvite: (id: number) => {
console.error('not implemented: removeTentativeInvite');
},
tentativeInvites: immutable.Set<number>(),
hasCarpool: false, hasCarpool: false,
setHasCarpool: (has: boolean) => { setHasCarpool: (has: boolean) => {
console.error('not implemented: setHasCarpool'); console.error('not implemented: setHasCarpool');

View File

@ -21,12 +21,7 @@ function EventSignup({
}) { }) {
const { user, latitude, longitude } = signup; const { user, latitude, longitude } = signup;
const me = useMe(); const me = useMe();
const { const { tentativeInvites, hasCarpool } = useContext(EventContext);
addTentativeInvite,
removeTentativeInvite,
tentativeInvites,
hasCarpool,
} = useContext(EventContext);
const extraDistance = useMemo(() => { const extraDistance = useMemo(() => {
if (myPlaceDetails != null && !(latitude === null || longitude === null)) { if (myPlaceDetails != null && !(latitude === null || longitude === null)) {
@ -66,10 +61,7 @@ function EventSignup({
myPlaceDetails, myPlaceDetails,
]); ]);
const isTentativelyInvited = useMemo( const isTentativelyInvited = signup.user.id in tentativeInvites;
() => tentativeInvites.has(signup.user.id),
[signup.user.id, tentativeInvites]
);
if (user.id === me?.id) { if (user.id === me?.id) {
return null; return null;
@ -98,12 +90,12 @@ function EventSignup({
{!hasCarpool && {!hasCarpool &&
(isTentativelyInvited ? ( (isTentativelyInvited ? (
<CancelIcon <CancelIcon
onClick={() => removeTentativeInvite(user.id)} onClick={() => delete tentativeInvites[user.id]}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
/> />
) : ( ) : (
<PersonAddIcon <PersonAddIcon
onClick={() => addTentativeInvite(user.id)} onClick={() => (tentativeInvites[user.id] = true)}
style={{ cursor: 'pointer' }} style={{ cursor: 'pointer' }}
/> />
))} ))}

View File

@ -14,24 +14,44 @@ function createEdgeForObject<T extends PlainJSObject>(
value: T, value: T,
setValue: Dispatch<SetStateAction<T>> setValue: Dispatch<SetStateAction<T>>
): T { ): T {
// @ts-expect-error return new Proxy(value, {
const edge: T = {}; set: (target, property, value) => {
for (let [key, keyValue] of Object.entries(value)) { setValue((v) => ({ ...v, [property]: value }));
const set = (next: SetStateAction<typeof keyValue>) => {
const v = typeof next === 'function' ? next(keyValue) : next;
setValue((value) => ({ ...value, [key]: v }));
};
Object.defineProperty(edge, key, { return true;
enumerable: true, },
configurable: false, // @ts-expect-error
get: () => createEdge(keyValue, set), get: (target, property: keyof T) => {
set: (v) => void setValue((value) => ({ ...value, [key]: v })), const keyValue = target[property];
}); const set = (next: SetStateAction<typeof keyValue>) => {
} const v = typeof next === 'function' ? next(keyValue) : next;
return edge; setValue((value) => ({ ...value, [property]: v }));
};
return createEdge(keyValue, set);
},
deleteProperty: (target, property) => {
setValue((v) => {
const newValue = { ...v };
// @ts-ignore
delete newValue[property];
return newValue;
});
return true;
},
});
} }
const inPlaceArrayOperations = [
'fill',
'reverse',
'push',
'pop',
'shift',
'unshift',
];
function createEdgeForArray<T extends PlainJS>( function createEdgeForArray<T extends PlainJS>(
value: PlainJSArray<T>, value: PlainJSArray<T>,
setValue: Dispatch<SetStateAction<PlainJSArray<T>>> setValue: Dispatch<SetStateAction<PlainJSArray<T>>>
@ -58,7 +78,8 @@ function createEdgeForArray<T extends PlainJS>(
if (typeof property === 'number') { if (typeof property === 'number') {
return target[property]; return target[property];
} else { } else {
if (typeof target[property] === 'function') { // @ts-ignore
if (inPlaceArrayOperations.includes(property)) {
return function () { return function () {
const newValue = [...value]; const newValue = [...value];
const method = newValue[property]; const method = newValue[property];
@ -67,9 +88,15 @@ function createEdgeForArray<T extends PlainJS>(
setValue(newValue); setValue(newValue);
return result; return result;
}; };
} else { } else if (typeof target[property] === 'function') {
return target[property]; return function () {
console.log(target[property]);
// @ts-ignore
return target[property].apply(target, arguments);
};
} }
return target[property];
} }
}, },
}); });