I started learning React and so I'm making an app that performs CRUD functions on activities. I'm using MobX lite to manage the app's state.
When I load activities using the loadActivities
method from the activityStore.ts
, the activities duplicate, so there's two of each. This also causes some unexpected behavior when I try to delete or edit them.
Now in my index.tsx
, the jsx is wrapped in React.StrictMode>
component, which, if removed, allows the app to function perfectly. Now I know <React.StrictMode>
component renders components twice in order to reveal bugs, but I don't want to just blindly remove it to make the app work without figuring out the issue.
What is the cause of this bug and how do I fix it?
I tried resetting the activities
property's length to 0 every time loadActivities
was called, but the issue remained.
index.tsx
import React from 'react';
import ReactDOM from 'react-dom/client';
import './app/layout/styles.css';
import App from './app/layout/App';
import reportWebVitals from './reportWebVitals';
import NavBar from './app/layout/NavBar';
import { Container } from 'reactstrap';
// Importing the Bootstrap CSS
import 'bootstrap/dist/css/bootstrap.css';
import { store, StoreContext } from './app/stores/store';
const root = ReactDOM.createRoot(
document.getElementById('root') as HTMLElement
);
root.render(
<React.StrictMode>
<NavBar />
<Container>
<StoreContext.Provider value={store}>
<App />
</StoreContext.Provider>
</Container>
</React.StrictMode>
);
// If you want to start measuring performance in your app, pass a function
// to log results (for example: reportWebVitals(console.log))
// or send to an analytics endpoint.
reportWebVitals();
App.tsx
import { observer } from 'mobx-react-lite';
import React from 'react';
import ActivityDashboard from '../../features/activities/dashboard/ActivityDashboard';
function App() {
return (
<div className="App">
<header className="App-header">
</header>
<ActivityDashboard />
</div>
);
}
export default observer(App);
ActivityDashboard.tsx
import { observer } from "mobx-react-lite";
import React, { useEffect } from "react";
import { Col, Row } from "reactstrap";
import { Activity } from "../../../app/models/Activity";
import { useStore } from "../../../app/stores/store";
import ActivityDashboardItem from "./ActivityDashboardItem";
import AddActivityModal from "./AddActivityModal"
export default observer(function ActivityDashboard() {
const { activityStore } = useStore();
useEffect(() => {
activityStore.loadActivities();
}, [activityStore]);
return (
<div>
<div className="d-flex justify-content-between align-items-center">
<h1>Activity Dashboard</h1>
<AddActivityModal />
</div>
<hr />
{activityStore.activities.length > 0 ? (
<Row xs="1" md="2" lg="3">
{activityStore.activities.map((activity: Activity) => (
<Col key={activity.id}>
<ActivityDashboardItem activity={activity} />
</Col>
))}
</Row>
) : (
<div className="text-center fs-3">There are no activities.</div>
)}
</div>
)
})
ActivityDashboardItem.tsx
import React, { useState } from 'react';
import EditActivityModal from './EditActivityModal';
import {
Badge,
Button,
Card, CardBody, CardSubtitle, CardText, CardTitle,
Modal, ModalHeader, ModalBody, ModalFooter
} from 'reactstrap';
import { Activity } from '../../../app/models/Activity';
import { useStore } from '../../../app/stores/store';
import { observer } from 'mobx-react-lite';
interface Props {
activity: Activity
}
export default observer(function ActivityDashboardItem({ activity }: Props) {
const { activityStore } = useStore();
const deleteThisActivity = () => {
var executeDelete = window.confirm("Delete the activity?");
if (executeDelete === false)
return;
activityStore.deleteActivity(activity.id);
}
const [viewModal, setViewModal] = useState(false);
const toggleViewModal = () => setViewModal(!viewModal);
return (
<div>
<Card className="mb-4 bg-light">
<CardBody>
<div className="d-flex justify-content-between">
<CardTitle tag="h5">{activity.title}</CardTitle>
<Badge color="secondary" className="align-self-start">{activity.category}</Badge>
</div>
<CardSubtitle className="text-muted" tag="h6">{activity.city}, {activity.venue}</CardSubtitle>
</CardBody>
<CardBody>
<CardText>{activity.description}</CardText>
<div className="d-flex justify-content-end">
<Button color="danger" onClick={deleteThisActivity} className="me-2">Delete</Button>
<div className="me-2">
<EditActivityModal activity={activity} />
</div>
<Button color="secondary" onClick={toggleViewModal}>View</Button>
</div>
</CardBody>
</Card>
<Modal isOpen={viewModal} toggle={toggleViewModal}>
<ModalHeader toggle={toggleViewModal}>{activity.title}</ModalHeader>
<ModalBody>{activity.description}</ModalBody>
<ModalFooter>
<Button color="secondary" onClick={toggleViewModal}>
Close
</Button>
</ModalFooter>
</Modal>
</div>
)
})
activityStore.ts
import axios from "axios";
import { makeAutoObservable, runInAction } from "mobx";
import { Activity } from "../models/Activity";
export default class ActivityStore {
activities: Activity[] = [];
loading = false;
loadingInitial = false;
constructor() {
makeAutoObservable(this);
}
// TODO figure out issue with duplicate data loading
loadActivities = async () => {
this.loadingInitial = true;
try {
console.log('load activities ran with the following activities', this.activities);
const activitiesResponse = await axios.get('http://localhost:5000/activities');
const activities: Activity[] = activitiesResponse.data;
activities.forEach((activity: Activity) => {
activity.date = activity.date.split('T')[0];
runInAction(() => {
this.activities.push(activity);
});
});
} catch (error) {
console.log(error);
} finally {
runInAction(() => {
this.loadingInitial = false;
});
}
}
deleteActivity = async (id: number) => {
try {
console.log('delete activity ran');
await axios.delete(`http://localhost:5000/activities/${id}`);
var indexOfActivity = this.activities.findIndex(x => x.id === id);
runInAction(() => {
this.activities.splice(indexOfActivity, 1);
});
} catch (error) {
console.log(error);
}
}
editActivity = async (editedActivity: Activity) => {
try {
await axios.put('http://localhost:5000/activities', editedActivity);
var indexOfActivity = this.activities.findIndex(x => x.id === editedActivity.id);
runInAction(() => {
this.activities[indexOfActivity] = editedActivity;
});
} catch (error) {
console.log(error);
}
}
addActivity = async (newActivity: Activity) => {
console.log(newActivity);
try {
const activitiesResponse = await axios.post('http://localhost:5000/activities', newActivity);
const addedActivity: Activity = activitiesResponse.data;
runInAction(() => {
this.activities.push(addedActivity);
});
} catch (error) {
console.log(error);
}
}
}
store.ts
import { createContext, useContext } from "react";
import ActivityStore from "./activityStore";
interface Store {
activityStore: ActivityStore
}
export const store: Store = {
activityStore: new ActivityStore()
}
export const StoreContext = createContext(store);
export function useStore() {
return useContext(StoreContext);
}