Professional-React-and-Next.js-Course
Professional-React-and-Next.js-Course copied to clipboard
A few improvements in CorpComment project
- Since we use Zustand to manage the state, there is no need to pass props to FeedbackForm and HashtagItem, we can just use Zustand in them directly.
- It's always a good idea to use custom hooks as explained in https://tkdodo.eu/blog/working-with-zustand and your video https://www.youtube.com/watch?v=I7dwJxGuGYQ&t=69s&ab_channel=ByteGrad. It's the same for other tools as for useContext. But because we drive the state companList and FilteredFeedbackItems inside the store, we must export getCompanyList() and getFilteredFeedbackItems() with () to make sure they are executed inside the store.
feedbackItemsStore.ts
import { create } from "zustand";
import { TFeedbackItem } from "../lib/types";
type Store = {
feedbackItems: TFeedbackItem[];
isLoading: boolean;
errorMessage: string;
selectedCompany: string;
actions: {
getCompanyList: () => string[];
getFilteredFeedbackItems: () => TFeedbackItem[];
addItemToList: (text: string) => Promise<void>;
selectCompany: (company: string) => void;
fetchFeedbackItems: () => Promise<void>;
};
};
export const useFeedbackItemsStore = create<Store>((set, get) => ({
feedbackItems: [],
isLoading: false,
errorMessage: "",
selectedCompany: "",
actions: {
getCompanyList: () => {
const state = get();
return state
.feedbackItems.map((item) => item.company)
.filter((company, index, array) => {
return array.indexOf(company) === index;
});
},
getFilteredFeedbackItems: () => {
const state = get();
return state.selectedCompany
? state.feedbackItems.filter(
(feedbackItem) => feedbackItem.company === state.selectedCompany
)
: state.feedbackItems;
},
addItemToList: async (text: string) => {
const companyName = text
.split(" ")
.find((word) => word.includes("#"))!
.substring(1);
const newItem: TFeedbackItem = {
id: new Date().getTime(),
text: text,
upvoteCount: 0,
daysAgo: 0,
company: companyName,
badgeLetter: companyName.substring(0, 1).toUpperCase(),
};
set((state) => ({
feedbackItems: [...state.feedbackItems, newItem],
}));
await fetch(
"https://bytegrad.com/course-assets/projects/corpcomment/api/feedbacks",
{
method: "POST",
body: JSON.stringify(newItem),
headers: {
Accept: "application/json",
"Content-Type": "application/json",
},
}
);
},
selectCompany: (company: string) => {
set(() => ({
selectedCompany: company,
}));
},
fetchFeedbackItems: async () => {
set(() => ({
isLoading: true,
}));
try {
const response = await fetch(
"https://bytegrad.com/course-assets/projects/corpcomment/api/feedbacks"
);
if (!response.ok) {
throw new Error();
}
const data = await response.json();
set(() => ({
feedbackItems: data.feedbacks,
}));
} catch (error) {
set(() => ({
errorMessage: "Something went wrong. Please try again later.",
}));
}
set(() => ({
isLoading: false,
}));
},
},
}));
export const useFeedbackItems = () => useFeedbackItemsStore((state) => state.feedbackItems);
export const useIsLoading = () => useFeedbackItemsStore((state) => state.isLoading);
export const useErrorMessage = () => useFeedbackItemsStore((state) => state.errorMessage);
export const useSelectedCompany = () => useFeedbackItemsStore((state) => state.selectedCompany);
export const useFeedbackItemActions = () => useFeedbackItemsStore((state) => state.actions);
export const useCompanyList = () => useFeedbackItemsStore((state) => state.actions.getCompanyList()); //Make sure it executes inside Zustand store.
export const useFilteredFeedbackItems = () => useFeedbackItemsStore((state) => state.actions.getFilteredFeedbackItems()); //Make sure it executes inside Zustand store.
FeedbackForm.tsx
import { useState } from "react";
import { MAX_CHARACTERS } from "../../lib/constants";
import { useFeedbackItemActions } from "../../stores/feedbackItemsStore";
export default function FeedbackForm() {
const {addItemToList} = useFeedbackItemActions();
const [text, setText] = useState("");
const [showValidIndicator, setShowValidIndicator] = useState(false);
const [showInvalidIndicator, setShowInvalidIndicator] = useState(false);
const charCount = MAX_CHARACTERS - text.length;
const handleChange = (event: React.ChangeEvent<HTMLTextAreaElement>) => {
const newText = event.target.value;
if (newText.length > MAX_CHARACTERS) {
return;
}
setText(newText);
};
const handleSubmit = (event: React.FormEvent<HTMLFormElement>) => {
event.preventDefault();
// basic validation
if (text.includes("#") && text.length >= 5) {
setShowValidIndicator(true);
setTimeout(() => setShowValidIndicator(false), 2000);
} else {
setShowInvalidIndicator(true);
setTimeout(() => setShowInvalidIndicator(false), 2000);
return;
}
addItemToList(text);
setText("");
};
return (
<form
onSubmit={handleSubmit}
className={`form ${showValidIndicator && "form--valid"} ${
showInvalidIndicator && "form--invalid"
}`}
>
<textarea
value={text}
onChange={handleChange}
id="feedback-textarea"
placeholder="blabla"
spellCheck={false}
/>
<label htmlFor="feedback-textarea">
Enter your feedback here, remember to #hashtag the company
</label>
<div>
<p className="u-italic">{charCount}</p>
<button>
<span>Submit</span>
</button>
</div>
</form>
);
}
FeedbackList.tsx
import {
useErrorMessage,
useFilteredFeedbackItems,
useIsLoading
} from "../../stores/feedbackItemsStore";
import ErrorMessage from "../ErrorMessage";
import Spinner from "../Spinner";
import FeedbackItem from "./FeedbackItem";
export default function FeedbackList() {
const isLoading = useIsLoading();
const errorMessage = useErrorMessage();
const filteredFeedbackItems = useFilteredFeedbackItems();
return (
<ol className="feedback-list">
{isLoading && <Spinner />}
{errorMessage && <ErrorMessage message={errorMessage} />}
{filteredFeedbackItems.map((feedbackItem) => (
<FeedbackItem key={feedbackItem.id} feedbackItem={feedbackItem} />
))}
</ol>
);
}
HashtagItem.tsx
import { useFeedbackItemActions } from "../../stores/feedbackItemsStore";
type HashtagItemProps = {
company: string;
};
export default function HashtagItem({
company,
}: HashtagItemProps) {
const { selectCompany } = useFeedbackItemActions();
return (
<li key={company}>
<button onClick={() => selectCompany(company)}>#{company}</button>
</li>
);
}
HashtagList.tsx
import { useCompanyList } from "../../stores/feedbackItemsStore";
import HashtagItem from "./HashtagItem";
export default function HashtagList() {
const companyList = useCompanyList();
return (
<ul className="hashtags">
{companyList.map((company) => (
<HashtagItem
key={company}
company={company}
/>
))}
</ul>
);
}
App.tsx
import { useEffect } from "react";
import { useFeedbackItemActions } from "../stores/feedbackItemsStore";
import HashtagList from "./hashtag/HashtagList";
import Container from "./layout/Container";
import Footer from "./layout/Footer";
function App() {
const { fetchFeedbackItems } = useFeedbackItemActions();
useEffect(() => {
fetchFeedbackItems();
}, [fetchFeedbackItems]);
return (
<div className="app">
<Footer />
<Container />
<HashtagList />
</div>
);
}
export default App;