Skip to content

Instantly share code, notes, and snippets.

@dmhalejr
Forked from cl0ckwork/PostFeed.tsx
Last active August 26, 2023 18:50
Show Gist options
  • Save dmhalejr/247286ed738d38162ad765a703220f9a to your computer and use it in GitHub Desktop.
Save dmhalejr/247286ed738d38162ad765a703220f9a to your computer and use it in GitHub Desktop.
Post Feed Refactor

Context

Imagine you are working for a "social" company that has a layout similar to facebook groups, with the following features listed below.

  • a user can join a group
  • a user must belong to an entity (ie: company)
  • a user can create posts in the group that are of different types (discussion or sales)
  • a user can view these posts from within the context of a group wall or feed, or within the context of their company or personal page
  • a user can edit a post once created.

Homework

Considering the features, please have a look at the PostFeed.tsx file which is an implementation of the component that wraps and renders the "feed" of the group. Please answer the below 2 questions in whatever format you'd like - preferably something that can display snippets (like markdown or a .tsx file).

  1. Pick at least 2 implementations you agree with, include a snippet and explain why you like and what you think the benefits/trade-offs are (this could be as simple as a hook implementation/use that you like).
  2. Pick 3 implementations you would refactor. Of the 3 you would refactor, please write some psuedo code and explain or demonstrate what you would change, and why. (This could be as complex or as simple as youd like)

Example (simple) - Please do not use this in your response.

const { sendMessage } = useSnackbar();

I like the above hook absraction for a snackbar, it provides a very simple interface for interacting with a, presumably, global implementation. As an engineer I can just grab it and go for my messaging use case. It also provides me a single location for updating a snackbar implementation.

There are no wrong/right answers - please keeop in mind this is how im getting insight into your thought process as an engineer

import React, { useState } from 'react';
import { useThemeStyles } from 'components/styles';
import { Post } from 'resources/domain/entity/IItem';
import useLang from 'components/hooks/useLang';
import { Box, Grid, makeStyles, Typography } from '@material-ui/core'
import { DropDown, MenuItem } from './CommonDropDown';
import { yupResolver } from '@hookform/resolvers/yup';
import InputCommentWrapper from 'components/utilities/InputCommentWrapper';
import { FormProvider, useForm } from 'react-hook-form';
import {
IItemInitialValues,
ItemCreateButtonState,
ItemValidationSchema,
} from 'components/views/GroupFeed';
import { getTextWithEllipsis } from 'util/getTextWithEllipsis';
import { CommonButton } from './CommonButton';
import { ChatBubbleOutline, LocalOffer } from '@material-ui/icons';
import { useSelector } from 'react-redux';
import { RootState } from 'state_management/reducers';
import { IItemDetailProps } from 'resources/domain/entity/IItem';
import {
ISaleItemDetailProps,
PostType,
} from 'resources/domain/entity/ISaleItem';
import userImage from 'assets/images/user-avatar.png';
import { useSnackbar } from 'components/notification/SnackbarNotification';
import { useMutation } from 'services/useMutation';
import { useSetUserInterestOnPusher } from 'services/notifications/pusher';
import PostFeed, { usePostCacheClearing } from '../views/GroupFeed/PostFeed';
import { GroupId } from 'src/services/groupService/models/groupModel';
import { PostTypeFilter } from 'components/views/GroupFeed/PostFeed';
import { RuleId } from 'src/services/rulesService/models/rules';
const CreateEngagementPost = React.lazy(
() => import('../views/CreateEngagementPost')
);
const CreateSalePost = React.lazy(() => import('../views/CreateSalePost'));
/**
* Styles logic for this component.
*/
const useStyles = makeStyles({
createPostCard: {
backgroundColor: '#fff',
borderRadius: '10px',
boxShadow: '0px 3px 6px #0000000D',
marginBottom: '15px',
paddingBottom: '10px',
},
createPostCardTop: {
borderBottom: '1px solid #707070',
padding: '15px',
'& h2': {
color: '#000',
fontSize: '22px',
fontWeight: 400,
height: '100%',
display: 'flex',
alignItems: 'center',
},
'& p': {
color: '#000',
fontSize: '22px',
fontWeight: 400,
'& a': {
color: '#000',
},
},
},
createPostCardMid: {
margin: '15px',
display: 'flex',
alignItems: 'center',
borderBottom: '1px solid #707070',
paddingBottom: '10px',
'& .MuiFormControl-root': {
width: '100%',
},
},
circleImage: {
'& img': {
borderRadius: '100%',
width: '50px',
height: '50px',
minWidth: '50px',
objectFit: 'cover',
objectPosition: 'center',
},
},
createPostCardBottom: {
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '15px',
'& span': {
color: '#707070',
display: 'flex',
alignItems: 'center',
marginRight: '5px',
cursor: 'pointer',
'& svg': {
width: '18px',
marginRight: '5px',
minWidth: '18px',
marginTop: '-3px',
},
},
},
});
/**
* This function handles all the posts in Group Feed view.
*/
const CommonGroupItemsList = ({
postType,
companyId,
groupId,
ruleId,
}: {
postType?: PostTypeFilter;
companyId?: string;
groupId: GroupId;
ruleId?: RuleId;
}): JSX.Element => {
const styleClasses = useStyles();
const commonStyleClasses = useThemeStyles();
const { f: translation } = useLang();
const { sendMessage } = useSnackbar();
const [loading, setLoading] = useState<boolean>();
const postCacheClearing = usePostCacheClearing();
const userInfo = useSelector((states: RootState) => states.authReducer.user);
const [showCreatePostModal, setShowCreatePostModal] = useState(false);
const [showCreateSalePostModal, setShowCreateSalePostModal] = useState(false);
const [item, setItem] = useState<Post>();
const { mutateAsync: updateSaleMutateAsync } = useMutation({
mutationKey: 'saleItemsAndOffers.updateSaleItem',
});
const { mutateAsync: createSaleMutateAsync } = useMutation({
mutationKey: 'saleItemsAndOffers.createSaleItem',
});
const { mutateAsync: createDiscussionMutateAsync } = useMutation({
mutationKey: 'items.createItemForGroup',
});
const { mutateAsync: updateDiscussionMutateAsync } = useMutation({
mutationKey: 'items.updateItemById',
});
// eventEmitter.subscribe(EventType.REFETCH_ITEM, 'refetch', () =>
// postsResponse.refetch()
// );
const ItemDropdown = [
{ id: 1, title: 'Discussion', icon: <ChatBubbleOutline /> },
{ id: 2, title: 'Sale', icon: <LocalOffer /> },
];
const { updateUserInterests } = useSetUserInterestOnPusher();
/**
* This function handles create post in a modal.
*/
const handleCreatePostModal = (id: string, value: boolean) => {
if (id === 'Discussion') setShowCreatePostModal(value);
if (id === 'Sale') setShowCreateSalePostModal(value);
setItem(undefined);
};
/**
* Setting Initial values for the Create Post
*/
const initialValues = {
title: '',
};
const methods = useForm({
resolver: yupResolver(ItemValidationSchema(translation)),
});
const {
handleSubmit,
reset,
watch,
formState: { errors },
} = methods;
/**
* This function creates the post.
*/
const onSubmit = async (
createItemPayload:
| {
postType: 'Discussion';
itemsData: IItemDetailProps;
isUpdate: boolean;
}
| {
postType: 'Sale';
itemsSaleData: ISaleItemDetailProps;
isUpdate: boolean;
}
) => {
if (!groupId) {
sendMessage({
message: translation('app.item.create.group.undefined').replace(
'postType',
createItemPayload.postType.toLowerCase()
),
});
return;
}
switch (createItemPayload.postType) {
case 'Discussion':
return (async () => {
const res = !createItemPayload.isUpdate
? await createDiscussionMutateAsync({
groupId,
data: createItemPayload.itemsData,
})
: item &&
(await updateDiscussionMutateAsync({
params: { groupId, itemId: item.id },
data: createItemPayload.itemsData,
}));
if ([res?.data?.code].includes(200)) {
sendMessage({
message: translation('app.item.message.create.success'),
severity: 'success',
});
reset({ ...initialValues });
postCacheClearing({
groupId,
// @ts-expect-error - these types are a mess
userId: userInfo.id,
// todo: find a better way to get this
// @ts-expect-error - companyId should not be optional on a user
companyId: userInfo.company.id,
});
// postsResponse.refetch();
handleCreatePostModal('Discussion', false);
updateUserInterests();
}
})();
case 'Sale':
return (async () => {
const res = !createItemPayload.isUpdate
? await createSaleMutateAsync({
groupId,
data: createItemPayload.itemsSaleData,
})
: item?.sale?.id
? await updateSaleMutateAsync({
groupId,
saleId: item?.sale?.id,
data: createItemPayload.itemsSaleData,
})
: undefined;
if ([res?.data?.code].includes(200)) {
sendMessage({
message: createItemPayload.isUpdate
? translation('app.item.message.update.success')
: translation('app.item.message.create.success'),
severity: 'success',
});
reset({ ...initialValues });
postCacheClearing({
groupId,
// @ts-expect-error - these types are a mess
userId: userInfo.id,
// todo: find a better way to get this
// @ts-expect-error - companyId should not be optional on a user
companyId: userInfo.company.id,
});
handleCreatePostModal('Sale', false);
updateUserInterests();
}
})();
default:
return ((_payload: never) => null)(createItemPayload);
}
};
/**
* The function gets the data from React hook form and creates discussion items in database
* If an item images are available then the function will upload it on S3 bucket and return a URL.
* It then appends the URL with the data and stores it in the database
*
* @param data
*/
const onCreateItem = async (data: IItemInitialValues) => {
setLoading(true);
const itemsData = {
title: data.title,
postType: PostType.DISCUSSION,
description: '',
commentsAllowed: true,
};
await onSubmit({ postType: 'Discussion', itemsData, isUpdate: false });
setLoading(false);
};
return (
<div>
{!companyId ? (
<Box className={styleClasses.createPostCard}>
<Box className={styleClasses.createPostCardTop}>
<Grid container>
<Grid xs={6}>
<Typography variant="h2">
{translation('app.item.create')}
</Typography>
</Grid>
<Grid xs={6}>
<Box className={commonStyleClasses.textRight}>
<DropDown
label={translation('app.feed.item.type.label')}
items={ItemDropdown}
onItemClick={(item: MenuItem) =>
handleCreatePostModal(item?.title as string, true)
}
/>
</Box>
</Grid>
</Grid>
</Box>
<FormProvider {...methods}>
<Box className={styleClasses.createPostCardMid}>
<Box className={styleClasses.circleImage}>
<img
src={userInfo.avatar ?? userImage}
alt={getTextWithEllipsis({ text: userInfo.name, limit: 10 })}
/>
</Box>
<InputCommentWrapper
name="title"
placeholder={translation('app.item.create.placeholder')}
type="text"
errorobj={errors as { [key: string]: { message: string } }}
multiline
minRows={1}
disabled={loading}
/>
<Typography align="right" className={commonStyleClasses.ml20}>
<CommonButton
size="medium"
label="Post"
onClick={handleSubmit(onCreateItem)}
loading={loading}
disabled={loading}
/>
</Typography>
</Box>
</FormProvider>
<Box className={styleClasses.createPostCardBottom}>
{ItemCreateButtonState?.map((info, index) => (
<span
key={index}
onClick={() => handleCreatePostModal('Discussion', true)}
>
{info?.icon}
{info?.label}
</span>
))}
</Box>
</Box>
) : null}
<PostFeed groupId={groupId} postType={postType} ruleId={ruleId} />
{showCreatePostModal ? (
<CreateEngagementPost
open={showCreatePostModal}
onCloseModal={() => {
handleCreatePostModal('Discussion', false);
reset();
}}
title={watch('title', '')}
onSubmit={onSubmit}
item={item}
/>
) : null}
{showCreateSalePostModal ? (
<CreateSalePost
open={showCreateSalePostModal}
onCloseModal={() => handleCreatePostModal('Sale', false)}
title={watch('title', '')}
onSubmit={onSubmit}
item={item}
/>
) : null}
</div>
);
};
export default CommonGroupItemsList;
  1. Pick at least 2 implementations you agree with, include a snippet and explain why you like and what you think the benefits/trade-offs are (this could be as simple as a hook implementation/use that you like).

Lazy loading of pertinent components

React.lazy paired with Suspense or ErrorHandler is a great way to reduce the initial bundle size and build in resiliency/improving performance of the application as you run into situations were it is necessary. I've found myself just introducing lazy loading as a default standard in an application as it grows from infancy while regarding deadlines. The trade-off is honestly trying to find the use case where it makes sense. For instance, I attempt to use the whole get it working, get it right, and (if necessary) get it performant. So some of performance improvments may not actively be seen or felt and add unnecessary complexity into your application if it stays small/straight forward

const CreateEngagementPost = React.lazy(
  () => import('../views/CreateEngagementPost')
);
const CreateSalePost = React.lazy(() => import('../views/CreateSalePost'));

Using Yup/React Hook Form

Yup is a great declarative way to use with react-hook-form to ensure the data we are submitting are sanitized and valid for our back-end. Love the way we can reuse complex schemas as is necessary elsewhere in the app. react-hook-form has been on my radar. Overall, it helps me get my job done in a easy/non-complex way while allowing lower boilerplate. Knowing the tanstack is taking over via react-query I'd plausibly use react-table but would need a spike to determine best use case.

const methods = useForm({
    resolver: yupResolver(ItemValidationSchema(translation)),
  });

// ... omited for brevity
  1. Pick 3 implementations you would refactor. Of the 3 you would refactor, please write some psuedo code and explain or demonstrate what you would change, and why. (This could be as complex or as simple as youd like)

1. Introduce Typescript aliases to make sure to get the imports standardized / Introduce TS definition files to ensure models are deferred globally

As the app grows in complexity the necessity for relative pathing coralling may be something we may need to handle. Depending on how the code base is setup this may become an unnecessary headache eventually.

/**
  In this case all the interfaces have been introduced into a *.d.ts file 
  Most of the time I've used TS we've dumped entities into appropriately named definition files (example: user.d.ts)
*/
import React, { useState } from 'react';
import { useSelector } from 'react-redux';
import { FormProvider, useForm } from 'react-hook-form';
import { Box, Grid, makeStyles, Typography } from '@material-ui/core';
import { ChatBubbleOutline, LocalOffer } from '@material-ui/icons';
import { yupResolver } from '@hookform/resolvers/yup'
import {
  useThemeStyles,
  useLang,
  useSetUserInterestOnPusher,
  useMutation,
  useSnackbar,
  usePostCacheClearing,
} from '@hooks';
import {
  getTextWithEllipsis
} from '@utils';

import {
  DropDown,
  MenuItem,
  InputCommentWrapper,
  CommonButton,
} from '@components';

import {
  RootState
} from '@state';

import {
  PostFeed
} from '@pages';

import {
  userImage
} from '@assets';
// ... more

2. If the service/useMutation is using react-query in the hook I'd advise registering isLoading, isFetching, etc. at that level and removing the need for local state to verify loading and opens up the possiblity of handling hoisting our snackBar hook/form reset logic into the returned onError and onSuccess.

  const {
    handleSubmit,
    reset,
    watch,
    formState: { errors },
  } = methods;

const { mutateAsync: updateSaleMutateAsync, isLoading: isUpdateSaleLoading } = useMutation({
    onMutate: () => {
      // optimistic updating! :shocked-pikachu-face:
    }, 
    onError: () => {
      sendMessage({
        message: translation('app.item.create.failure').replace(
          'postType',
          createItemPayload.postType.toLowerCase()
        ),
      });
    },
    onSuccess: () => {
      sendMessage({
        message: translation('app.item.create.success').replace(
          'postType',
          createItemPayload.postType.toLowerCase()
        ),
      });
      reset();
    },
    mutationKey: 'saleItemsAndOffers.updateSaleItem',
  });

3. I'd advise checking on our data structure in the larger application structure to figure out if there may be a way to normalize the data to assist in minimizing complexity regarding the branching logic in the component itself. That could easily be something regarding adding an additional entry where if id exists then do update. Minimize the complexity of the onSubmit entry.

// for technical discussion.
{
  id: string | null, // this would detemine create/update [uuid preferable]
  type: POST_TYPE, // enum 
  itemsData: IItemDetailProps; // is this nesting necessary? Feels like both sales/discussion would both have threading     available.
}

/** 
A user's post may have multiple type but the entity itself is just a post to delination between create/update of a discussion/sales is interesting. I'd check my assumption and see if those two sales/discussion type of posts are truly that different to defer a duplication of update/creation actions. 
*/

// This is in the yada yada yada future where all my assumptions were challenged and then returned successful

  /**
   * This function creates the post.
   */
  const onSubmit = async (
    createItemPayload: ICreateItemPayload) => {
    // my earlier refactor would have the payload in a *.d.ts file somewhere (or ideally auto generate my types via the back-end)
    // if (!groupId) checking should be on the back-end routes and failure captured in the react-query onError
    // my earlier refactor would also handle the reset of the form on success 

    const { id } = createItemPayload;

    if (!id) {
      await createMutateAsync({
        groupId,
        data: createItemPayload.itemsData,
      })
    } else {
      await updateMutateAsync({
        groupId,
        data; createItemPayload.itemsData,
      })
    }
  };
@dmhalejr
Copy link
Author

This was a fun little exercise and I appreciate your time in reading through this! 👍 Hope you find your best freelancer! 👏

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment