I'm a beginner in React and MobX and I'm working on a project where I need to manage the state of compared companies. I want to make sure I'm following best practices and not contradicting the MobX documentation.
I have a ComparisonButton component that allows users to add or remove companies from a comparison list. I'm using MobX to manage the state of compared companies. Below is my implementation:
ComparisonButton Component:
const ComparisonButton = ({ companyId, isCompared }) => {
const store = useStore();
const axiosAuth = useAxiosAuth();
const notification = useNotificationCtx();
const [isLoading, setIsLoading] = useState(false);
useEffect(() => {
if (isCompared !== store.isCompanyCompared(companyId)) {
store.setIsCompared(companyId, isCompared);
}
}, []);
const localIsCompared = store.isCompanyCompared(companyId);
const addToComparison = async () => {
try {
const response = await axiosAuth.post("/api/add", { companyId });
store.setIsCompared(companyId, true);
} catch (error) {
} finally {
}
};
const deleteFromComparison = async () => {
try {
const response = await axiosAuth.post("/api/delete", { companyId });
store.setIsCompared(companyId, false);
} catch (error) {
} finally {
}
};
return (
<Button
onClick={localIsCompared ? deleteFromComparison : addToComparison}
icon={<BiSolidAddToQueue color={localIsCompared ? "#1677ff" : "#71717a"} size={20} />}
/>
);
};
export default observer(ComparisonButton);
MobX Store:
import { makeAutoObservable } from "mobx";
class Store {
comparedCompanies = new Map();
constructor() {
makeAutoObservable(this);
}
setIsCompared(companyId, isCompared) {
if (this.comparedCompanies.get(companyId) !== isCompared) {
this.comparedCompanies.set(companyId, isCompared);
console.log(`Company ${companyId} comparison state changed to ${isCompared}`);
}
}
isCompanyCompared(companyId) {
return this.comparedCompanies.get(companyId) || false;
}
}
const store = new Store();
export default store;
My Questions: Is my use of useEffect to initialize the comparison state correct? I'm using it to set the initial comparison state when the component mounts. Am I correctly managing the comparison state with MobX? Specifically, the methods setIsCompared and isCompanyCompared. Are there any improvements or best practices I'm missing that would make my MobX integration more efficient or idiomatic? Any advice or feedback on this implementation would be greatly appreciated.
Thank you!
I implemented the ComparisonButton component using React and MobX as shown above. My expectation was that the component would correctly update the comparison state for each company and reflect the changes in the UI.
The component works as expected in most cases, but I'm not entirely sure if my approach to setting and getting the comparison state with MobX follows best practices. Specifically, I want to confirm:
I believe you overcomplicated the logic and I am not exactly sure what the parent component looks like and how it is managing it's state, but I think this is a more clear approach:
// ComparisonButton Component:
const ComparisonButton = ({ companyId, initialIsCompared }) => {
const store = useStore();
const isCompared = store.isCompanyCompared(companyId);
const isInComparingAction = store.isCompanyInComparingAction(companyId);
useEffect(() => {
store.setIsCompared(companyId, initialIsCompared);
}, []);
return (
<Button
onClick={() => store.changeCompanyComparison(companyId, !isCompared)}
icon={<BiSolidAddToQueue color={isCompared ? "#1677ff" : "#71717a"} size={20} />}
disabled={isInComparingAction}
/>
);
};
// MobX Store
class Store {
comparedCompanies = {};
companiesInComparingAction = {};
constructor() {
makeAutoObservable(this);
}
changeCompanyComparison(companyId, newComparisonValue) {
const endpoint = newComparisonValue ? "/api/add" : "/api/delete";
this.companiesInComparingAction[companyId] = true;
axiosAuth
.post(endpoint, { companyId })
.then(() => {
this.setIsCompared(companyId, newComparisonValue);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
this.companiesInComparingAction[companyId] = false;
});
}
setIsCompared(companyId, isCompared) {
this.comparedCompanies[companyId] = isCompared;
console.log(`Company ${companyId} comparison state changed to ${newComparisonValue}`);
}
isCompanyCompared(companyId) {
return this.comparedCompanies[companyId] || false;
}
isCompanyInComparingAction(companyId) {
return this.companiesInComparingAction[companyId] || false;
}
}
Now the ComparisonButton component is simply displaying the data and using the actions from the store. The store itself contains all the logic.
If your goal was to read from the props if the company was already compared and save it in your store on the first render then yes, you used your effect correctly, just a little bit overcomplicated.
Btw you may iterate over your companies in the parent element and set the comparisons in advance in the store instead of prop drilling them and setting them via useEffect on a deep child level (for example after you fetch them). That would be my additional advice if it suits your case.
In case your companies come with the flag isCompared from the endpoint it would be advisable to be reworked something like this:
// ComparisonButton Component:
const ComparisonButton = ({ company }) => {
const store = useStore();
const isInComparingAction = store.isCompanyInComparingAction(company);
return (
<Button
onClick={() => store.changeCompanyComparison(company)}
icon={<BiSolidAddToQueue color={company.isCompared ? "#1677ff" : "#71717a"} size={20} />}
disabled={isInComparingAction}
/>
);
};
// MobX Store (full example)
class Store {
constructor() {
makeAutoObservable(this);
}
allCompanies = [];
isLoadingCompanies = false;
isInitialLoadingCompanies = true;
loadCompaniesError = null;
companiesInComparingAction = {};
get comparedCompanies() {
return this.allCompanies.filter((c) => c.isCompared);
}
get nonComparedCompanies() {
return this.allCompanies.filter((c) => !c.isCompared);
}
loadCompanies() {
this.isLoadingCompanies = true;
this.loadCompaniesError = false;
axiosAuth
.get("/api/companies")
.then((response) => {
this.allCompanies = response;
})
.catch((error) => {
this.loadCompaniesError = JSON.stringify(error);
})
.finally(() => {
this.isLoadingCompanies = false;
if (this.isInitialLoadingCompanies) {
this.isInitialLoadingCompanies = false;
}
});
}
get isLoadingCompaniesData() {
return this.isLoadingCompanies || this.isInitialLoadingCompanies;
}
changeCompanyComparison(company) {
const newComparisonValue = !company.isCompared;
const endpoint = newComparisonValue ? "/api/add" : "/api/delete";
this.companiesInComparingAction[company.id] = true;
axiosAuth
.post(endpoint, { companyId: company.id })
.then(() => {
company.isCompared = newComparisonValue;
console.log(`Company ${company.id} comparison state changed to ${newComparisonValue}`);
})
.catch((error) => {
console.error(error);
})
.finally(() => {
this.companiesInComparingAction[company.id] = false;
});
}
isCompanyInComparingAction(company) {
return this.companiesInComparingAction[company.id] || false;
}
}