I got two react components. The first component is managing a list of the second component after making a call to my api and I tried to put a delete button to remove one component from the list.
I got a weird behavior : when I click on the remove document button the document is set to an empty array. And I got a log of an empty array for the documents.
Parent component :
export const DocumentEnvelope: React.FC<DocumentEnvelopeProps> = (props) => {
const [documents, setDocuments] = useState([]);
useEffect(() => {
setDocuments([]);
axios.get("/myurl").then(response => {
response.data.forEach(document => {
setDocuments((doc) => [...doc, <Document name={document.name} removeDocument={removeDocument}/>]);});
});
}, []);
const removeDocument = (name) => {
console.log(documents);
setDocuments(documents.filter(item => item.name !== name));
};
return (
<>
{documents}
</>
);
};
Child component :
interface DocumentProps {
removeDocument,
name: string
}
export const Document: React.FC<DocumentProps> = (props) => {
return (
<div>
My document
<button onClick={() => props.removeDocument(props.name)}>
Remove document
</button>
</div>
);
};
<Document name=document.name removeDocument={removeDocument}/>
This part is missing curly braces around document.name, consider using a different variable name as document is used to refer to the html document in javascript. I can also recommend storing the data in the state but not the components themselves.
You're filtering on the documents array as if it consisted of document objects. But you have set the documents array to list of Document React Elements.
Let your documents array consist pure JS objects i.e. a document as you're getting it from response instead of React elements. Use map in JSX to loop over documents and return <Document.../> elements.
I mean like the following :-
export const DocumentEnvelope: React.FC<DocumentEnvelopeProps> = (props) => {
const [documents, setDocuments] = useState([]);
useEffect(() => {
axios.get("/myurl").then(response => {
setDocuments(response.data);
}, []);
const removeDocument = (name) => {
setDocuments(documents.filter(document => document.name !== name));
};
return (
<>
{documents.map((document)=>
<Document name={document.name} removeDocument={removeDocument}/>
)}
</>
);
};
Related
I wrote a program that takes and displays contacts from an array, and we have an input for searching between contacts, which we type and display the result.
I used if in the search function to check if the searchKeyword changes, remember to do the filter else, it did not change, return contacts and no filter is done
I want to do this control with useEffect and I commented on the part I wrote with useEffect. Please help me to reach the solution of using useEffect. Thank you.
In fact, I want to use useEffect instead of if
I put my code in the link below
https://codesandbox.io/s/simple-child-parent-comp-forked-4qf39?file=/src/App.js:905-913
Issue
In the useEffect hook in your sandbox you aren't actually updating any state.
useEffect(()=>{
const handleFilterContact = () => {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
};
return () => contacts;
},[searchKeyword]);
You are returning a value from the useEffect hook which is interpreted by React to be a hook cleanup function.
See Cleaning up an effect
Solution
Add state to MainContent to hold filtered contacts array. Pass the filtered state to the Contact component. You can use the same handleFilterContact function to compute the filtered state.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const [filtered, setFiltered] = useState(contacts.slice());
const setValueSearch = (e) => setSearchKeyword(e.target.value);
useEffect(() => {
const handleFilterContact = () => {
if (searchKeyword.length >= 1) {
return contacts.filter((contact) =>
contact.fullName.toLowerCase().includes(searchKeyword.toLowerCase())
);
} else {
return contacts;
}
};
setFiltered(handleFilterContact());
}, [contacts, searchKeyword]);
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts} filter={filtered} />
</div>
);
};
Suggestion
I would recommend against storing a filtered contacts array in state since it is easily derived from the passed contacts prop and the local searchKeyword state. You can filter inline.
const MainContent = ({ contacts }) => {
const [searchKeyword, setSearchKeyword] = useState("");
const setValueSearch = (e) => setSearchKeyword(e.target.value);
const filterContact = (contact) => {
if (searchKeyword.length >= 1) {
return contact.fullName
.toLowerCase()
.includes(searchKeyword.toLowerCase());
}
return true;
};
return (
<div>
<input
placeholder="Enter a keyword to search"
onChange={setValueSearch}
/>
<Contact contacts={contacts.filter(filterContact)} />
</div>
);
};
I am using React context to pass down data, following the docs, however I am stuck with the initial value and am not sure what I did wrong.
This is what my context file looks like:
export const ItemsContext = createContext([]);
ItemsContext.displayName = 'Items';
export const ItemsProvider = ({ children }) => {
const [items, setItems] = useState([]);
const [loading, setLoading] = useState(false);
const setData = async () => {
setLoading(true);
setItems(await getItemsApiCall());
setLoading(false);
};
useEffect(() => {
setData();
}, []);
console.warn('Items:', items); // This shows the expected values when using the provider
return (
<ItemsContext.Provider value={{ items, loading }}>
{children}
</ItemsContext.Provider>
);
};
Now, I want to feed in those items into my app in the relevant components. I am doing the following:
const App = () => {
return (
<ItemsContext.Consumer>
{(items) => {
console.warn('items?', items); // Is always the initial value of "[]"
return (<div>Test</div>);
}}
</ItemsContext.Consumer>
);
}
However, as my comment states, items will always be empty. On the other hands, if I do just use the ItemsProvider, I do get the proper value, but at this point need to access it directly on the app, so the ItemsContext.Consumer seems to make more sense.
Am I doing something wrong here?
Edit: A way around it seems to be to wrap the Consumer with the Provider, but that feels wrong and didn't see that at the docs. Is that perhaps the case?
So essentially, something like this:
const App = () => {
return (
<ItemsProvider>
<ItemsContext.Consumer>
{(items) => {
console.warn('items?', items); // Is always the initial value of "[]"
return (<div>Test</div>);
}}
</ItemsContext.Consumer>
</ItemsProvider>
);
}
You have to provide a ItemsContext provider above the App component hierarchy,otherwise the default value of the context will be used.
something in this form:
<ItemsContext.Provider value={...}>
<App/>
</ItemsContext.Provider>
TL/DR: Having trouble referencing items in array by index (using React), could use some guidance.
I am attempting to create a component on my SPA out of data coming from an API. Using React hook useState and useEffect I have created state, done an axios call, and then set the response.data.articles to state (.articles is the array of objects I am using to create the dynamic content).
function App() {
const [storyArray, setStoryArray] = useState();
useEffect(() => {
axios.get('http://newsapi.org/v2/everything?domains=wsj.com&apiKey=[redacted_key_value]')
.then((response) => {
// console.log(response);
setStoryArray(response.data.articles);
})
.catch((err) => {
console.log(err);
})
}, [])
console.log(storyArray)
return (
<div className="App">
<Directory />
<HeaderStory />
</div>
);
}
From here, my state is an array of objects. My goal is to pass THE FIRST object as props to the component <HeaderStory /> but any time I attempt to reference this array item with dot notation I am met with an undefined error. My attempt at circumventing this is problem was to set the item to a variable and then pass the variable as props to the component.
const firstStory = storyArray[0];
This also resulted in an undefined error. Looking for advice / assistance on referencing items in an array to be passed and used in React.
On the first render the storyArray will have no value/undefined, The useEffect hook will execute only after component mount.
So you have to render the component conditionally, if the storyArray has value then only render the HeaderStory.
Example:
function App() {
const [storyArray, setStoryArray] = useState();
useEffect(() => {
axios.get('http://newsapi.org/v2/everything?domains=wsj.com&apiKey=[redacted_key_value]')
.then((response) => {
// console.log(response);
setStoryArray(response.data.articles);
})
.catch((err) => {
console.log(err);
})
}, [])
return (
<div className="App" >
<Directory />
{storyArray && <HeaderStory firstStory={storyArray[0]} />}
</div>
);
}
You should init default value for storyArray.
Example code:
function App() {
const [storyArray, setStoryArray] = useState([]); //Init storyArray value
useEffect(() => {
axios.get('http://newsapi.org/v2/everything?domains=wsj.com&apiKey=[redacted_key_value]')
.then((response) => {
// console.log(response);
setStoryArray(response.data.articles);
})
.catch((err) => {
console.log(err);
})
}, [])
console.log(storyArray)
return (
<div className="App">
<Directory />
<HeaderStory firstStory={storyArray[0] || {}} />
</div>
);
}
I set props firstStory={storyArray[0] || {}} because if storyArray[0] is undefined then pass empty object "{}" for firstStory prop.
Lets say I have a components array in my React app:
const deleteProject = useCallback(project => {
// something
}, []);
return (
projects.map(p => (
<button onClick={() => deleteProject(p)}>Delete</button>
);
);
Is there any way I could use just deleteProject function without wrapping it into separate callbacks i.e. {} => {} for each component? This is for performance purposes. I mean something like:
<button onClick={deleteProject}>Delete</button>
And then in deleteProject somehow I'd need to determine which project to delete, but how? It only takes click event as argument
If you have a long projects list and see performance issues you could define DeleteButton component to avoid button re-rendering
const DeleteButton = ({project, deleteProject}) => {
const onClick = useCallback(
() => deleteProject(project),
[project, deleteProject],
);
return <button onClick={onClick}>Delete</button>
}
const YourComponent = ({projects, deleteProject}) => (
<>
{projects.map(project => <DeleteButton {...{project, deleteProject}}/>)
</>
)
You can achieve by assigning project identifier to button as below
const deleteProject = event => {
projectId = event.target.id;
// Delete project here using id
}
return (
projects.map(p => (
<button id={p.id} onClick={deleteProject}>Delete</button>
);
I'm building a simple toast notification system using React Context. Here is a link to a simplified but fully working example which shows the problem https://codesandbox.io/s/currying-dust-kw00n.
My page component is wrapped in a HOC to give me the ability to add, remove and removeAll toasts programatically inside of this page. The demo has a button to add a toast notification and a button to change the activeStep (imagine this is a multi-step form). When the activeStep is changed I want all toasts to be removed.
Initially I did this using the following...
useEffect(() => {
toastManager.removeAll();
}, [activeStep]);
...this worked as I expected, but there is a react-hooks/exhaustive-deps ESLint warning because toastManager is not in the dependency array. Adding toastManager to the array resulted in the toasts being removed as soon as they were added.
I thought I could have fixed that using useCallback...
const stableToastManager = useCallback(toastManager, []);
useEffect(() => {
stableToastManager.removeAll();
}, [activeStep, stableToastManager]);
...however, not only does this not work but I would rather fix the issue at the source so I don't need to do this every time I want this kind of functionality, as it is likely to be used in many places.
This is where I am stuck. I'm unsure as to how to change my Context so that I don't need add additional logic in the components that are being wrapped by the HOC.
export const ToastProvider = ({ children }) => {
const [toasts, setToasts] = useState([]);
const add = (content, options) => {
// We use the content as the id as it prevents the same toast
// being added multiple times
const toast = { content, id: content, ...options };
setToasts([...toasts, toast]);
};
const remove = id => {
const newToasts = toasts.filter(t => t.id !== id);
setToasts(newToasts);
};
const removeAll = () => {
if (toasts.length > 0) {
setToasts([]);
}
};
return (
<ToastContext.Provider value={{ add, remove, removeAll }}>
{children}
<div
style={{
position: `fixed`,
top: `10px`,
right: `10px`,
display: `flex`,
flexDirection: `column`
}}
>
{toasts.map(({ content, id, ...rest }) => {
return (
<button onClick={() => remove(id)} {...rest}>
{content}
</button>
);
})}
</div>
</ToastContext.Provider>
);
};
export const withToastManager = Component => props => {
return (
<ToastContext.Consumer>
{context => {
return <Component toastManager={context} {...props} />;
}}
</ToastContext.Consumer>
);
};
If you want to "Fix it from the core", you need to fix ToastProvider:
const add = useCallback((content, options) => {
const toast = { content, id: content, ...options };
setToasts(pToasts => [...pToasts, toast]);
}, []);
const remove = useCallback(id => {
setToasts(p => p.filter(t => t.id !== id));
}, []);
const removeAll = useCallback(() => {
setToasts(p => (p.length > 0 ? [] : p));
}, []);
const store = useMemo(() => ({ add, remove, removeAll }), [
add,
remove,
removeAll
]);
Then, the useEffect will work as expected, as the problem was that you re-initialized the ToastProvider functionality on every render when it needs to be a singleton.
useEffect(() => {
toastManager.removeAll();
}, [activeStep, toastManager]);
Moreover, I would recommend to add a custom hook feature as the default use case, and providing wrapper only for class components.
In other words, do not use wrapper (withToastManager) on functional components, use it for classes, as it is considered an anti-pattern, you got useContext for it, so your library should expose it.
// # toastContext.js
export const useToast = () => {
const context = useContext(ToastContext);
return context;
};
// # page.js
import React, { useState, useEffect } from 'react';
import { useToast } from './toastContext';
const Page = () => {
const [activeStep, setActiveStep] = useState(1);
const { removeAll, add } = useToast();
useEffect(() => {
removeAll();
}, [activeStep, removeAll]);
return (
<div>
<h1>Page {activeStep}</h1>
<button
onClick={() => {
add(`Toast at ${Date.now()}!`);
}}
>
Add Toast
</button>
<button
onClick={() => {
setActiveStep(activeStep + 1);
}}
>
Change Step
</button>
</div>
);
};
export default Page;