013 React CRUD - CarrieKroutil/Reactivities GitHub Wiki
As we start to implement the CRUD operations on the frondend side, now is a good time to review what folder structure to use.
React is not opinionated, but does have a couple normal ways:
- Group by features or routes with a common folder for cross cutting concerns.
- Group by file type like apis and commponents
To learn more, read this FAQ: https://legacy.reactjs.org/docs/faq-structure.html.
This repo is going to use features to organize by, so create the new folder and file structure under "..\client-app\src":
- app
- layout
- Move "App.tsx" file to this location (root component - entry into app)
- Move "index.css" and then rename to "styles.css"
- layout
- features
Delete the "App.css" and "App.test.tsx" files.
Check the imports updated as needed in the "..\client-app\src\index.tsx" by resolving any import errors. E.g.
-
import App from './App';
should now beimport App from './app/layout/App';
-
import './index.css';
should now beimport './app/layout/styles.css';
Confirm the app still runs as expected.
First, while the api is running, view the API documentation via http://localhost:5000/swagger/index.html.
Look at the response for the GET list of activities and copy it and paste it into a free tool like https://transform.tools/json-to-typescript to get the ts version. E.g.
[
{
"id": "3fa85f64-5717-4562-b3fc-2c963f66afa6",
"title": "string",
"date": "2023-08-18T18:31:22.280Z",
"description": "string",
"category": "string",
"city": "string",
"venue": "string"
}
]
export type Root = Root2[]
export interface Root2 {
id: string
title: string
date: string
description: string
category: string
city: string
venue: string
}
Then add a new folder and file under "..\client-app\src\app" called "models\activity.ts". Paste in the above typescript and rename Root2 to Activity
Update "..\client-app\src\app\layout\App.tsx" to use the new model in state and remove the : any
code in the return. E.g.
const [activities, setActivities] = useState<Activity[]>([]);
return (
<div>
<Header as='h2' icon='users' content='activities'/>
<List>
{activities.map(activity => (
<List.Item key={activity.id}>
{activity.title}
</List.Item>
))}
</List>
</div>
);
Tada!! Now there is type safety in our frontend code.
Axios can also enforce type safety if you update the get request as follows:
axios.get<Activity[]>
Using Semantic UI's menu component, create a new file ..\client-app\src\app\layout\NavBar.tsx
with contents:
// Required if we want to return jsx
import React from "react";
import { Button, Container, Menu } from "semantic-ui-react";
// React components are really just functions that return jsx
export default function NavBar() {
return (
<Menu inverted fixed='top'>
<Container>
<Menu.Item header>
<img src="/assets/logo.png" alt="logo" style={{marginRight: 10}} />
Reactivities
</Menu.Item>
<Menu.Item name='Activities'></Menu.Item>
<Menu.Item>
<Button positive content='Create Activity' />
</Menu.Item>
</Container>
</Menu>
)
}
Then update ..\client-app\src\app\layout\App.tsx
to no longer have header and use the new NavBar component:
// Remove
<Header as='h2' icon='users' content='activities'/>
// Add NavBar with auto imported using statement and return following:
import NavBar from './NavBar';
return (
// Only allowed to return a single element per React component, so using <Fragment> to group the NavBar and Container into one element.
// Note: an empty <> element is the shortcut for fragment.
<>
<NavBar />
<Container style={{marginTop: '7em'}}>
<List>
{activities.map(activity => (
<List.Item key={activity.id}>
{activity.title}
</List.Item>
))}
</List>
</Container>
</>
);
Run the app to confirm the new menu is working via two terminals: api --> dotnet run
and client-app --> npm start
.
To get the logo image working, add a new "assets" folder under ..\client-app\public
and add logo.png
along with the other assets needed soon.
Add folders and file as follows - ..\client-app\src\features\activities\dashboard\ActivityDashboard.tsx
and then paste in content:
import React from "react";
import { Grid, List } from "semantic-ui-react";
import { Activity } from "../../../app/models/activity";
interface Props {
activities: Activity[];
}
// Could pass in props: Props and then everything would need to reference props (E.g. props.activities) first, or using {activities}: Props will destructure the property passed in
export default function ActivityDashboard({activities}: Props) {
return (
<Grid>
// SemanticUI has a 16 column grid (not 12 like other libs)
<Grid.Column width='10'>
<List>
{activities.map(activity => (
<List.Item key={activity.id}>
{activity.title}
</List.Item>
))}
</List>
</Grid.Column>
</Grid>
)
}
Then update ..\client-app\src\app\layout\App.tsx
with just this line inside the Container:
<ActivityDashboard activities={activities} />
To further extract the list component away, create a new file - ActivityList.txs with the following:
import React from "react";
import { Activity } from "../../../app/models/activity";
import { Button, Item, Label, Segment } from "semantic-ui-react";
import { act } from "react-dom/test-utils";
interface Props {
activities: Activity[];
}
export default function ActivityList({activities}: Props) {
return(
// Segments give padding and background color
// https://react.semantic-ui.com/views/item - divided puts in a horizontal line between items
<Segment>
<Item.Group divided>
{activities.map(activity => (
<Item key={activity.id}>
<Item.Content>
<Item.Header as='a'>{activity.title}</Item.Header>
<Item.Meta>{activity.date}</Item.Meta>
<Item.Description>
<div>{activity.description}</div>
<div>{activity.city}, {activity.venue}</div>
</Item.Description>
<Item.Extra>
<Button floated='right' content='View' color='blue' />
<Label basic content={activity.category} />
</Item.Extra>
</Item.Content>
</Item>
))}
</Item.Group>
</Segment>
)
}
Then update ActivityDashboard to use the new component inside the Grid.Column element: <ActivityList activities={activities}></ActivityList>
.
In the App.tsx, add:
// useState is defined as either an Activity object or unioned "|" to be undefined and set to that as default.
const [selectedActivity, setSelectedActivity] = useState<Activity | undefined>(undefined);
Then add functions to handle the selection and cancelation of an activity:
function handleSelectActivity(id: string) {
setSelectedActivity(activities.find(x => x.id === id));
}
function handleCancelSelectActivity(){
setSelectedActivity(undefined);
}
Still in App.tsx, update the ActivityDashboard component returned to be able to pass along the state and new functions:
return (
// Only allowed to return a single element per React component, so using <Fragment> to group the NavBar and Container into one element.
// Note: an empty <> element is the shortcut for fragment.
<>
<NavBar />
<Container style={{marginTop: '7em'}}>
<ActivityDashboard
activities={activities}
selectedActivity={selectedActivity}
selectActivity={handleSelectActivity}
cancelSelectActivity={handleCancelSelectActivity}
/>
</Container>
</>
);
Now the ActivityDashboard interface needs to be updated to know how to handle the state.
Fun tip, put curser on the ActivityDashboard component in App.tsx and press F12 to navigate to it's definition.
Update the props and the destructured properties passed in, as well as pass down the selectActivity to the list component (note that will need to be updated next to resolve the error):
import React from "react";
import { Grid, List } from "semantic-ui-react";
import { Activity } from "../../../app/models/activity";
import ActivityList from "./ActivityList";
import ActivityDetails from "../details/ActivityDetails";
import ActivityForm from "../form/ActivityForm";
interface Props {
activities: Activity[];
selectedActivity: Activity | undefined;
selectActivity: (id: string) => void;
cancelSelectActivity: () => void;
}
export default function ActivityDashboard({activities, selectedActivity,
selectActivity, cancelSelectActivity}: Props) {
return (
<Grid>
<Grid.Column width='10'>
<ActivityList activities={activities} selectActivity={selectActivity}></ActivityList>
</Grid.Column>
<Grid.Column width='6'>
{activities[0] &&
<ActivityDetails activity={activities[0]}></ActivityDetails>}
<ActivityForm></ActivityForm>
</Grid.Column>
</Grid>
)
}
Add the selectActivity function to the ListActivity component's interface and props passed into the function:
interface Props {
activities: Activity[];
selectActivity: (id: string) => void;
}
export default function ActivityList({activities, selectActivity}: Props) {
...
Add the onClick event to the list component's "View" button via onClick={() => selectActivity(activity.id)}
. Note, it is important to use the () =>
aka arrow function to not execute the function when the component first loads, but rather wait until the click action.
Update the ActivityDashboard component to use the selectedActivity:
<Grid.Column width='6'>
{selectedActivity &&
<ActivityDetails activity={selectedActivity} cancelSelectActivity={cancelSelectActivity}></ActivityDetails>}
<ActivityForm></ActivityForm>
</Grid.Column>
Now the ActivityDetail component needs to be informed of the cancelSelectActivity function and include it in the destructured props passed into the function:
interface Props {
activity: Activity;
cancelSelectActivity: () => void;
}
export default function ActivityDetails({activity, cancelSelectActivity}: Props) {
...
Update the "Cancel" button to have onClick={cancelSelectActivity}
Test the changes and now the "View" button should display the related details of selected item.