This is an exercise for React & Typescript by building a very simple Twitter.
There is no backend involved and all the data is stored locally.
This is a Next.js project bootstrapped with create-next-app
.
Run the development server:
npm run dev
# or
yarn dev
Open http://localhost:3000 with your browser to see the result.
- React with Functional Components and Hooks
- Typescript
- Nextjs project bootstrapper
- emotion CSS-in-JS support
- RuetifyUI self-hosted UI lib adherent to Material-UI and Vuetify
-
Timeline Page
- includes the home page(/home) and the user page(/{username}), the home page displays all the tweets, the user page displays only the tweets of the user
- redirect the index page(/) to the home page(/home)
-
Register & Login & Logout
- unique username password without security
- as options in the left-side AppBar of timeline pages(/)(/{username})
- start with the home page(/) of a user named @tourist if a user hasn't logged in
- keep the logged-in status in local session after a user successfully registers or logs in
- transfer to her/his timeline page(/{username}) after a user successfully registers or logs in
- transfer to the home page(/) of a user named @tourist after a user logs out
-
Tweets in the timeline page
- each tweet owns a unique incremental integer ID
- users can post text tweets
- tweets are in a descending order
- tweets can be edited only by the posters
- tweets can be deleted only by the posters
- transfer to the detail page(/tweet/{tweet ID}) after clicking a tweet
-
AppBar
- appears in leftside of the home page(/home) and the user page(/{username})
- users can post a tweet quickly by clicking the Tweet Button
- users can go to the home page(/) by clicking the Home Navigation Button
- users can go to her/his timeline page(/{username}) by clicking the Profile Navigation Button
-
Tweets in the detail page
This little project is basically out of these principles:
- Readable Code Comes 1st
- Reusable Code Comes 2nd
Now let's look through the practices of above principles.
Component = (Props) => UI
Here are 3 types of Components in the project.
Type | Role | Dependencies |
---|---|---|
Generic Component | Atom utility | none / other Generic Component |
Common Component | Reusable business | Generic Component |
Page Component | Usual business, Chores | Generic & Common Component |
As the most atomic Components, Generic Components serve as the reinforcement from UI lib.
You can find them in @/components/generic and @/ui.
A UI Library Should Help to Reduce the Mental Burden and the Possibility of Making Mistakes
Take the Component Textfield for example.
You can find the practices in some forms like @/components/modals/SignInCard/SignIn/PasswordForm.tsx.
type TPasswordForm = {
username: string;
onAfterSubmit: Function;
};
function PasswordForm(props: React.PropsWithChildren<TPasswordForm>) {
return (
<>
<Textfield
id="username-disabled-input"
prompt="Phone, email, or username"
defaultValue={props.username}
disabled
/>
<Textfield
id="password-input"
ref={passwordTextfieldRef}
prompt="Password"
secretly
onChange={handleInputPassword}
/>
</>
);
}
Textfield in PasswordForm:
What does Textfield do?
- Its appearance and behavior are aligned with a UI lib refering to Material Design and mym-UI.
- It provides a reusable input which owns HTML attributes such as ref and defaultValue.
- It provides readable props such as secretly which means
<input type="password" />
.
As the reusable business Components, Common Component are dumb waiting to receive props.
They have nothing to do with State or Store in spite of containing a fair amount of reusable business logic.
NOTE:
Why some dumb Components are not called Common Components?Some dumb Components are so simple to be written that they serve as Widgwets around their parent Components Like Avatar.
Some Components are too specific in business to be reused across the Pages like PasswordForm.
You can use Common Components safely and update State or Store in their parent Components.
You can find them in @/components/common.
Take the Component TweetEditor for example.
You can find the practices in the scenes where users need to edit their tweets like @/components/timeline/MainContentCard/TweetEditorCard.
function TweetEditorCard() {
const { state: userState, dispatch: userDispatch } = useContext(UserStore);
const { state: tweetState, dispatch: tweetDispatch } = useContext(TweetStore);
//use a ref to control the inner <textarea>
const textareaRef = useRef<HTMLTextAreaElement>(null);
const handleCreateTweet = () => {
const elTextarea = textareaRef.current;
//make sure input valid
if (elTextarea == null || elTextarea.value === "") return;
//use the ref to get value from the inner <textarea>
const content = elTextarea.value;
const user: Poster = {
nickname: userState.nickname,
username: userState.username,
avatarUrl: userState.avatarUrl,
};
//make a POST request to create a Tweet
API.Tweet.postCreateTweet({ content, user }).then((tid) => {
//update the Store when successful
const doCreateTweet: TweetActions = {
type: TweetActionTypes.Create,
payload: _genTweetInstance(tid, content, user),
};
tweetDispatch(doCreateTweet);
//use the ref to control the inner <textarea>
elTextarea.value = "";
});
};
return (
<TweetEditor
ref={textareaRef}
user={userState}
submitButtonMetaText="Tweet"
textareaId="main-tweet-editor-textarea"
textareaPlaceholder="What's happening?"
onSubmit={handleCreateTweet}
/>
);
}
TweetEditor when creating tweets:
TweetEditor when updating tweets:
What does TweetEditor do?
- It is a large compound Component which combines Page Components including Avatar and Tools and Generic Components including AutosizeTextarea and Button.
- It is reusable across the Big Page Components TweetEditorCard from timeline which allows to write tweets in Timeline Page and EditorCard from modals which allows to post tweets quickly from the left-side AppBar or edit tweets' content in each Tweet.
- It is dumb therefore you always mess with Store and API in its parent Components.
As the logic elements of Pages, Page Components are "people" of the Page "nation".
They are everywhere and serve as all kinds of jobs.
You can find them in @/components/timeline and @/components/modals.
CONVENTION:
Why many Page Components are named as "xxCard"?They are "smart" or "container" which dealing with API and Store.
I choose to name them all as "Card" instead of "Container"
CONVENTION:
What are Widgets for?For a "Card" Page Component there are some small dumb Page Components and constant varibles like STYLE or ICON.
They are regarded as "Widgets" put aside the regarding Page Components like TopBannerCard's Widgets
Generic Components are useful "bricks" when building Page Components especially Unstyled Components such as Box.
If you need an Avatar, let's try it with inline CSS styles:
function SimpleAvatar(props: { url: string }) {
return (
<div
style={{
marginRight: "12px",
width: "48px",
height: "48px",
borderRadius: "50%",
background: `no-repeat url(${props.url})`,
backgroundSize: "contain",
}}
></div>
);
}
It is a little tiring to write these styles but it's OK.
But you feel shity when you need a LARGER Avatar, so you seal this with Props adding some "sweet" short names for a relief:
type TSweetAvatar = {
url: string;
m?: string; //margin
w?: number; //width
h?: number; //height
};
function SweetAvatar(props: TSweetAvatar) {
return (
<div
style={{
margin: props.m,
width: props.w ? `${props.w}px` : "48px",
height: props.h ? `${props.h}px` : "48px",
borderRadius: "50%",
background: `no-repeat url(${props.url})`,
backgroundSize: "contain",
}}
></div>
);
}
//require a larger width and height
const LargerAvatarCase = () => (
<SweetAvatar url="xxx" m="0 12px 0 0" w={64} h={64}></SweetAvatar>
);
How about making a Generic Component supports all the CSS properties?
Here the Unstyled Component Box comes:
import Box from "@/components/generic/containers/Box";
const LargerAvatarUsingBoxCase = () => (
<Box
m="0 12px 0 0"
w={64}
h={64}
borderRadius={"50%"}
bg="no-repeat url(xxx)"
bgSize="contain"
></Box>
);
No need to write an Avatar any more, just write styles into Props as you want.
You can refer to sxProps to check the supported CSS properties of Unstyled Components including Box, Button, Text.
Babysitter = (...args) => Component with some Props
Unstyled Components are powerful but not enough:
- Cumbersome that components receive style object as props in JSX -> To seperate styles from JSX
- Box with styles is not READABLE -> To name components individually or not
- Need some custom props in different situations -> To provide REUSABLE custom components
Here the Component Babysitter genCustomBox and defineCustomBox.
import { genCustomBox } from "@/components/generic/containers/Box";
const AvatarUsingCustomBoxCase = () => {
//a Container {display: flex; flex-direction: column;}
const Wrapper = genCustomBox({ vertical: true }, {});
const MyAvatar = genCustomBox(
{ noFlex: true }, //display: block;
{
w: 48,
h: 48,
p: "2px", //padding
borderRadius: "50%",
bg: `no-repeat url(xxx)`,
bgSize: "contain",
}
);
return (
<Wrapper>
<MyAvatar />
<MyAvatar />
<MyAvatar />
</Wrapper>
);
};
Now your Unstyled Components own their names Wrapper
and MyAvatar
which is easier to understand what they are for.
You use a TCustomBox object as the 1st argument.
-> To provide some short-cut settings like flex-direction
or box-sizing
You use a sxProps object as the 2nd argument.
-> To provide CSS properties as you want
import {
defineCustomBox,
genCustomBox,
} from "@/components/generic/containers/Box";
const AvatarUsingDefinedBoxCase = () => {
//provide Box with {display: block}
const genBlockBox = defineCustomBox({ noFlex: true });
const Wrapper = genBlockBox();
const MyAvatar = genBlockBox({
w: 48,
h: 48,
p: "2px", //padding
borderRadius: "50%",
bg: `no-repeat url(xxx)`,
bgSize: "contain",
});
return (
<Wrapper>
<MyAvatar />
<MyAvatar />
<MyAvatar />
</Wrapper>
);
};
By default genCustomBox
provides you with a display: flex
, you have to specify for each if you don't want.
Now by using defineCustomBox
you can customize your genCustomBox
and using genCustomBox
to customize your Box
.
Wonderful!
import { genCustomButton } from "@/components/generic/Button";
const FollowButton = genCustomButton({
variant: "plain",
depressed: true,
borderRadius: 18,
rippleDisabled: true,
height: 36,
padding: "0 16px",
backgroundColor: "rgb(15, 20, 25)",
hoverBackgroundColor: "rgb(39, 44, 48)",
inner: {
color: "#fff",
lineHeight: 18,
fontSize: 15,
fontWeight: 700,
letterSpacing: "normal",
},
});
function InteractionButtonGroup(props: TInteractionButtonGroup) {
const Wrapper = genCustomBox();
const handleSelect = () => props.onSelect("follow");
return (
<Wrapper>
<FollowButton onClick={handleSelect}>Follow</FollowButton>
</Wrapper>
);
}
type TInteractionButtonGroup = {
onSelect: (value: string) => void,
};
FollowButton in UserProfileCard:
In spite of the given Unstyled Component Button, let's make a Button
using genCustomBox
which is based on Box.
import { genCustomBox } from "@/components/generic/containers/Box";
const NotifyButton = genCustomBox(
{},
{
mr: "8px",
mb: "12px",
h: 34,
w: 34,
borderRadius: 9999,
border: "1px solid rgb(207, 217, 222)",
hoverBg: "rgba(15, 20, 25,0.1)",
transition: "background 0.15s ease",
cursor: "pointer",
JC: "center",
AI: "center",
}
);
function InteractionButtonGroup(props: TInteractionButtonGroup) {
const Wrapper = genCustomBox();
const handleSelect = () => props.onSelect("notify");
return (
<Wrapper>
<NotifyButton onClick={handleSelect}>
<Icon svg={IconNotify} />
</NotifyButton>
</Wrapper>
);
}
type TInteractionButtonGroup = {
onSelect: (value: string) => void,
};
NotifyButton in UserProfileCard: