0

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);
}
Lukas
  • 1,699
  • 1
  • 16
  • 49
  • Could you please include the code for your store as well? That should help identify this issue. – Abhinav Ramkumar Aug 03 '23 at 14:20
  • @AbhinavRamkumar Totally forgot to do that. It's included now. – Lukas Aug 03 '23 at 14:25
  • Its possible that calling runInAction twice within an action is what's causing the duplication. Could you try removing runInAction in your `finally` block and check if the issue is resolved. – Abhinav Ramkumar Aug 03 '23 at 14:32
  • Thanks for the suggestion. Unfortunately, I am still getting the same issue. – Lukas Aug 03 '23 at 14:45
  • One more possibility: Create an action called empty Activities: `emptyActivities = () => { this.activities = []; }` Add it into the useEffect on ActivityDashboard.tsx like so `return () => activityStore.emptyActivities();` – Abhinav Ramkumar Aug 03 '23 at 14:50
  • Tried but no luck. I also previously tried to wrap `this.activities = [ ];` and `this.activities.length = 0` in `runInAction` method within the `loadActivities` method. Logically, it seems like it should work but it just doesn't. The `console.log` method I have in the `activityStore.ts` logs twice, but both times the array has the same duplicated activities. – Lukas Aug 03 '23 at 15:05
  • Do you experience the same behavior when not running the app in dev mode? I think it should only be in dev mode. https://react.dev/reference/react/StrictMode – MatterOfFact Aug 03 '23 at 16:18
  • @MatterOfFact Trying to figure out how to run the app locally in production mode, but as I mentioned in the question, without `` the issue does not occur. – Lukas Aug 03 '23 at 16:29
  • Yes, it is a feature that it renders twice in dev mode with React.StrictMode. Check the doc links above – MatterOfFact Aug 03 '23 at 17:31

0 Answers0