I want to know how improve this calls in order to not repeat always the same sentence to refresh the state...
I don't need any huge refactor, only inputs like: you need to put this call inside a function and call it when you want... something like this...
export const CategoriesPage = () => {
const [categories, setCategories] = useState<Category[]>([]);
const [showModal, setShowModal] = useState(false);
const handleCreateCategory = (newCategory: CategoryCreate, file: File) => {
createCategoryHelper(newCategory, file)
.then(() => {
getCategoriesHelper().then(setCategories);
})
.finally(() => handleClose());
};
const handleDeleteCategory = (categoryId: Id) => {
SwalHelper.delete().then(() => {
deleteCategoryHelper(categoryId).then(() =>
getCategoriesHelper().then(setCategories)
);
});
};
const handleClose = () => {
setShowModal(false);
};
const handleModal = () => {
setShowModal(true);
};
useEffect(() => {
getCategoriesHelper().then(setCategories);
}, []);
return (
<>
<PageTitle title="Categories" />
<FilterBar>
<Button type="button" background="green" onClick={handleModal}>
+ Add new
</Button>
</FilterBar>
{showModal && (
<ModalPortal onClose={handleClose}>
<CreateCategoryForm
createCategory={(category, file: File) => {
handleCreateCategory(category, file);
}}
/>
</ModalPortal>
)}
<ListGrid columns={3}>
{categories.map((category) => {
const { id: categoryId } = category;
return (
<CategoryCard
key={categoryId}
{...category}
onClick={() => handleDeleteCategory(categoryId)}
/>
);
})}
</ListGrid>
</>
);
};
When component is mounting, on useEffect, fills the state with response in order to create a list.
When a category is created, I call to setState again to refresh the list.
Same on delete, on then, refresh again to update the list.
Three times calling the same sentence
getCategoriesHelper().then(setCategories)
This is getCategoriesHelper:
export const getCategoriesHelper = async () => {
const service = new CategoryServiceImplementation(apiConfig);
const uploadImageService = new AmplifyS3Service();
const repository = new CategoryRepositoryImplementation(
service,
uploadImageService
);
const useCase = new GetCategoriesUseCaseImplementation(repository);
return await useCase.getCategories();
};
Is there any way to make this code much cleaner and reusable?
Thanks in advance!
Everything is write, and all calls are made as they are designed to do
Related
Im a newbie in React and Im creating a simple form that sends data to DB. I made it work almost as I wanted, the only problem is that I dont know how to update the state which has an array inside.
The idea is to make a form so I can add recipes which include the whole recipe data that I map through to render each recipe. In the data object I need simple strings most of the time but then I need also three arrays or objects, I prefer the arrays in this case.
I found many solutions for class components but still I could figure out how to update the arrays. I even figured out how to update one array from a string input separated only with commas, then .split(', ') and .trim() and map() through but I could not setFormFields({}) at two places at the same time since the createRecipe() is async. The split just did not happen before the array was sent to the DB as a string. Thats why I dont put the whole code here.
I will simplify the code to make you see clear.
const defaultFormFields = {
title: '',
imageUrl: '',
leadText: '',
};
const NewRecipeForm = () => {
const [formFields, setFormFields] = useState(defaultFormFields);
const { title, imageUrl, leadText } = formFields;
const [ingredients, setIngredients] = useState([])
const handleFormFieldsChange = (event) => {
setFormFields({ ...formFields, [event.target.name]: event.target.value })
}
const handleIngredientsChange = ( event) => {
**// here I need help**
setIngredients()
}
const addIngredient = () => {
setIngredients([...ingredients, ''])
}
const removeIngredient = (index) => {
**// here I need help**
}
const createRecipe = async (event) => {
event.preventDefault()
// addRecipe sends the object to Firestore DB
addRecipe('recipes', url, formFields)
resetFormFields()
}
const resetFormFields = () => {
setFormFields(defaultFormFields);
};
return (
<NewRecipeFormContainer>
<h1>New recipe</h1>
<form onSubmit={createRecipe}>
<h1>Title</h1>
<input label='Title' placeholder='Recipe title' name='title' value={title} onChange={handleChange} />
<input label='imageUrl' placeholder='imageUrl' name='imageUrl' value={imageUrl} onChange={handleFormFieldsChange} />
<input label='leadText' placeholder='leadText' name='leadText' value={leadText} onChange={handleFormFieldsChange} />
<h1>Ingredients</h1>
**// here I need help ?**
{
ingredients.map((ingredient, index) => {
return (
<div key={index}>
<input label='Ingredience' placeholder='Ingredience' name='ingredient' value={ingredient.ingredient} onChange={handleChange} />
**// here I need help ?**
<button onClick={removeIngredient} >remove</button>
</div>
)
})
}
<button onClick={addIngredient} >add</button>
</form>
<Button onClick={createRecipe}>ODESLAT</Button>
</NewRecipeFormContainer>
)
}
I will appreciate any hint or help. Ive been totally stuck for two days. Thank you!
Here's an example of how to update a single element in a list.
const updateSingleItemInList = (index, update) => {
setList(list.map((l, i) => i === index ? update : l));
};
const add = (element) => setList([...list, element]);
Try simplifying your state first:
const [ingredients, setIngredients] = useState([]);
const [tips, setTips] = useState([]);
Then it becomes simple to write the handlers:
const updateIngredient = (index, text) => {
setIngredients(list.map((ing, i) => i === index ? text : ing));
};
const addIngredient = () => setIngredients([...ingredients, ""]);
Then you can create the form object when the user wants to submit:
addRecipe('recipes', url, {
ingredients: ingredients.map(i => ({ingredients: i})),
// etc.
});
Put it all together and here is the minimum viable example of a component that manages a dynamic number of form elements (tested, works):
export const TextBody = () => {
const [list, setList] = useState([{ name: "anything" }]);
const add = () => setList(l => [...l, { name: "" }]);
const remove = i => setList(l => [...l.slice(0, i), ...l.slice(i + 1)]);
const update = (i, text) => setList(l => l.map((ll, ii) => (ii === i ? { name: text } : ll)));
return (
<>
<TouchableOpacity onPress={add}>
<Text text="add" />
</TouchableOpacity>
{list.map((l, i) => (
<>
<Text text={JSON.stringify(l)} />
<TouchableOpacity onPress={() => remove(i)}>
<Text text="remove" />
</TouchableOpacity>
<Input onChange={c => update(i, c.nativeEvent.text)} />
</>
))}
</>
);
};
You can return those CRUD functions and the state from a custom hook so you only have to write this once in a codebase.
Edit: Just for fun, here's the same component with a reusable hook:
const useListOfObjects = (emptyObject = {}, initialState = []) => {
const [list, setList] = useState(initialState);
const add = () => setList(l => [...l, emptyObject]);
const remove = i => setList(l => [...l.slice(0, i), ...l.slice(i + 1)]);
const update = (i, text, field) =>
setList(l => l.map((ll, ii) => (ii === i ? { ...ll, [field]: text } : ll)));
return {
list,
add,
remove,
update,
};
};
export const TextBody = () => {
const { list, add, remove, update } = useListOfObjects({ name: "", id: Math.random() });
return (
<>
<TouchableOpacity onPress={add}>
<TextBlockWithShowMore text="add" />
</TouchableOpacity>
{list.map((l, i) => (
<React.Fragment key={`${l.id}`}>
<TextBlockWithShowMore text={JSON.stringify(l)} />
<TouchableOpacity onPress={() => remove(i)}>
<TextBlockWithShowMore text="remove" />
</TouchableOpacity>
<Input onChange={c => update(i, c.nativeEvent.text, "name")} />
</React.Fragment>
))}
</>
);
};
What I would like to happen is when displayBtn() is clicked for the items in localStorage to display.
In useEffect() there is localStorage.setItem("localValue", JSON.stringify(myLeads)) MyLeads is an array which holds leads const const [myLeads, setMyLeads] = useState([]); myLeads state is changed when the saveBtn() is clicked setMyLeads((prev) => [...prev, leadValue.inputVal]);
In DevTools > Applications, localStorage is being updated but when the page is refreshed localStorage is empty []. How do you make localStorage persist state after refresh? I came across this article and have applied the logic but it hasn't solved the issue. I know it is something I have done incorrectly.
import List from './components/List'
import { SaveBtn } from './components/Buttons';
function App() {
const [myLeads, setMyLeads] = useState([]);
const [leadValue, setLeadValue] = useState({
inputVal: "",
});
const [display, setDisplay] = useState(false);
const handleChange = (event) => {
const { name, value } = event.target;
setLeadValue((prev) => {
return {
...prev,
[name]: value,
};
});
};
const localStoredValue = JSON.parse(localStorage.getItem("localValue")) ;
const [localItems] = useState(localStoredValue || []);
useEffect(() => {
localStorage.setItem("localValue", JSON.stringify(myLeads));
}, [myLeads]);
const saveBtn = () => {
setMyLeads((prev) => [...prev, leadValue.inputVal]);
// setLocalItems((prevItems) => [...prevItems, leadValue.inputVal]);
setDisplay(false);
};
const displayBtn = () => {
setDisplay(true);
};
const displayLocalItems = localItems.map((item) => {
return <List key={item} val={item} />;
});
return (
<main>
<input
name="inputVal"
value={leadValue.inputVal}
type="text"
onChange={handleChange}
required
/>
<SaveBtn saveBtn={saveBtn} />
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
);
}
export default App;```
You've fallen into a classic React Hooks trap - because using useState() is so easy, you're actually overusing it.
If localStorage is your storage mechanism, then you don't need useState() for that AT ALL. You'll end up having a fight at some point between your two sources about what is "the right state".
All you need for your use-case is something to hold the text that feeds your controlled input component (I've called it leadText), and something to hold your display boolean:
const [leadText, setLeadText] = useState('')
const [display, setDisplay] = useState(false)
const localStoredValues = JSON.parse(window.localStorage.getItem('localValue') || '[]')
const handleChange = (event) => {
const { name, value } = event.target
setLeadText(value)
}
const saveBtn = () => {
const updatedArray = [...localStoredValues, leadText]
localStorage.setItem('localValue', JSON.stringify(updatedArray))
setDisplay(false)
}
const displayBtn = () => {
setDisplay(true)
}
const displayLocalItems = localStoredValues.map((item) => {
return <li key={item}>{item}</li>
})
return (
<main>
<input name="inputVal" value={leadText} type="text" onChange={handleChange} required />
<button onClick={saveBtn}> Save </button>
<button onClick={displayBtn}>Display Leads</button>
{display && <ul>{displayLocalItems}</ul>}
</main>
)
I need to detect if handleSelectProduct is being called in another component.
My problem is that if I want the child component(ProductDetailsComponent) to rerender, it still outputs the console.log('HELO'). I only want to output the console.log('HELO') IF handleSelectProduct is being click only.
const ProductComponent = () => {
const [triggered, setTriggered] = React.useState(0);
const handleSelectProduct = (event) => {
setTriggered(c => c + 1);
};
return (
<div>
Parent
<button type="button" onClick={handleSelectProduct}>
Trigger?
</button>
<ProductDetailsComponent triggered={triggered} />
</div>
);
};
const ProductDetailsComponent = ({ triggered }) => {
React.useEffect(() => {
if (triggered) {
console.log('HELO');
}
}, [triggered]);
return <div>Child</div>;
};
ReactDOM.render(
<ProductComponent />,
document.getElementById("root")
);
The simplest solution sounds to me by using an useRef to keep the old value, thus consider the console.log only when the triggered value changes.
const ProductDetailsComponent = ({ triggered }) => {
const oldTriggerRef = React.useRef(0);
React.useEffect(() => {
if (triggered !== oldTriggerRef.current) {
oldTriggerRef.current = triggered;
console.log('HELO');
}
}, [triggered]);
return <div>Child</div>;
};
The Code below successfully displays record from Atlassian Jira Storage API
//import goes here
const fetchRec = async () => {
const data = await storage.query().where('key', startsWith('Mykeysxxxxx')).getMany();
return data.results;
};
const App = () => {
const [projects] = useState(fetchRec);
return (
<div>
// display or map projects records here
{projects.map(project => (
<div>
<b>Fullname: {project.fullname}</b>
<b>Email: {project.email}</b>
</div>
))}
</div>
);
}
Here is my Issue. I need to refresh the Records when new data is inserted.
So I implemented the code below. when I click on refresh Records button, the new inserted record is not updated
<Button text="Refresh Records" onClick={async () => { await reloadRec(); }} />
async function reloadRec() {
fetchRec().then(projects);
//const [projects] = useState(async () => await fetchRec());
}
Here is the full code efforts so far
// Import goes here
const fetchRec = async () => {
const data = await storage.query().where('key', startsWith('Mykeysxxxxx')).getMany();
return data.results;
};
async function reloadRec() {
fetchRec().then(projects);
//const [projects] = useState(async () => await fetchRec());
}
const App = () => {
const [projects] = useState(fetchRec);
return (
<div>
// display or map projects records here
{projects.map(project => (
<div>
<b>Fullname: {project.fullname}</b>
<b>Email: {project.email}</b>
</div>
))}
<Button text="Refresh Records" onClick={async () => { await reloadRec(); }} />
</div>
);
}
This is the problem:
async function reloadRec() {
fetchRec().then(projects);
//const [projects] = useState(async () => await fetchRec());
}
In the above function, projects is undefined. And even if it was the same variable as defined in the App function, it's not a function that the then clause would invoke.
I think this is closer to what you want and follows the standard practices.
const App = () => {
const [projects, setProjects] = useState([]);
const fetcher = async () => {
const data = await storage.query().where('key', startsWith('Mykeysxxxxx')).getMany();
setProjects(data.results);
};
// do the initial fetch with an effect
useEffect(() => {
fetcher();
}, []);
return (
<div>
// display or map projects records here
{projects.map(project => (
<div>
<b>Fullname: {project.fullname}</b>
<b>Email: {project.email}</b>
</div>
))}
<Button text="Refresh Records" onClick={fetcher} />
</div>
);
}
InOrder to update information on the DOM you should update a state and DOM will show latest information. You bind and setRecord with fetchData function
Extanding
const App = () => {
const [projects,setProjects] = useState([]);
const fetchRecords = async() => {
const data = await /*... your data base query */
// Assuming projects is an array of project
setProjects(data.projects);
}
useEffect(()=>{
fetchRecords();
},[])
return (
<div>
// display or map projects records here
{projects.map(project => (
<div>
<b>Fullname: {project.fullname}</b>
<b>Email: {project.email}</b>
</div>
))}
<Button text="Refresh Records" onClick={fetchRecords} />
</div>
);
}
So at first when Component is mount you will see Projects list and then when you click on reload fetchRecords is again called resulting in state change and will reflect on dom
I have a simple useEffect hook in my Task:
const TaskAD = ({ match }: TaskADProps) => {
const { taskName } = match.params;
const [task, setTask] = useState<TaskData | null>(null);
const [loading, setLoading] = useState(true);
const authCommunicator = authRequest();
useEffect(() => {
const getTask = async () => {
const taskData = await authCommunicator
.get(`/task/${taskName}`)
.then((response) => response.data);
setTask(taskData);
setLoading(false);
};
getTask();
}, []);
if (loading || task == null) {
return <Spinner centered />;
}
const updateDescription = async (content: string): Promise<boolean> => {
const r = await authCommunicator
.patch(`/task/${task.name}/`, {
description: content,
})
.then((response) => {
console.log("Setting Task data!");
setTask(response.data);
return true;
})
.catch(() => false);
return r;
};
return (
<ProjectEntity name={taskName}>
<Space direction="vertical" size="small" style={{ width: "100%" }}>
<StatusRow status="Open" />
<TaskDetails task={task} />
<Description content={task.description} onSubmit={updateDescription} />
<Title level={2}>Subtasks:</Title>
<Table dataSource={dataSource} columns={columns} />
</Space>
</ProjectEntity>
);
};
Task object contains a description. The description is another component with a text area. The idea is: when a user changes the description in the child component, the child component has a function (passed via props) to update the description.
So I pass updateDescription to my child component (Description) via props. Both useEffect and updateDescription are in my Task component, the Description component is basically stateless. What happens:
user updates a description
child component calls the function, it updates a record in my DB
it gets the response from the API and calls setTask
task variable is passed to Description via props in Task's render, so they both get updated since the state of parent Task has changed
I see updated description
The only problem is that although it work, but when I do this, I can see this in console:
Setting Task data!
Warning: Can't perform a React state update on an unmounted component. This is a no-op, but it indicates a memory leak in your application. To fix, cancel all subscriptions and asynchronous tasks in a useEffect cleanup function.
(i've added the console.log just to see when it happens).
So I wanted to ask if this is a problem of me having async calls outside useEffect or maybe something else?
#Edit Description code (I removed all the unnecessary junk):
interface DescriptionProps {
content: string;
onSubmit?: (content: string) => Promise<boolean>;
title?: string;
rows?: number;
}
const Description = (props: DescriptionProps) => {
const { content, onSubmit, title, rows } = props;
const [descriptionContent, setDescriptionContent] = useState(content);
const [expanded, setExpanded] = useState(true);
const [editMode, setEditMode] = useState(false);
const [descriptionChanged, setDescriptionChanged] = useState(false);
const editable = onSubmit !== undefined;
const resetDescription = () => {
setDescriptionContent(content);
setDescriptionChanged(false);
};
const changeDescription = (value: string) => {
setDescriptionContent(value);
setDescriptionChanged(true);
};
const descriptionTitle = (
<>
<S.DescriptionTitle>{title}</S.DescriptionTitle>
</>
);
return (
<Collapse
defaultActiveKey={["desc"]}
expandIcon={S.ExpandIcon}
onChange={() => setExpanded(!expanded)}
>
<S.DescriptionHeader header={descriptionTitle} key="desc">
<S.DescriptionContent
onChange={(event): void => changeDescription(event.target.value)}
/>
{descriptionChanged && onSubmit !== undefined ? (
<S.DescriptionEditActions>
<Space size="middle">
<S.SaveIcon
onClick={async () => {
setDescriptionChanged(!(await onSubmit(descriptionContent)));
}}
/>
<S.CancelIcon onClick={() => resetDescription()} />
</Space>
</S.DescriptionEditActions>
) : null}
</S.DescriptionHeader>
</Collapse>
);
};
#Edit2
Funny thing, adding this to my Description solves the issue:
useEffect(
() => () => {
setDescriptionContent("");
},
[content]
);
Can anyone explain why?