013 React CRUD - CarrieKroutil/Reactivities GitHub Wiki

Folder Structure

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"
  • 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 be import App from './app/layout/App';
  • import './index.css'; should now be import './app/layout/styles.css';

Confirm the app still runs as expected.

GET using Type Safety

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[]>

Nav Bar

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.

Dashboard

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>.

Hook up "View" Button

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.

⚠️ **GitHub.com Fallback** ⚠️