I have the following scenario, i am mapping files with a Media Card Component
const [files, setFiles] = useState([]);
const thumbs = files.map((file, i) => (
<MediaCard
onClick={() => handleCardClick(i)}
checked={file.checked}
/>
));
Then i have a method which make my MediaCard Component checked in case it is not.
const handleCardClick = (index) => {
const newFiles = files.map((file, i) => {
if (i === index) file.checked = !file.checked
return file
});
setFiles(newFiles);
}
Now i need a button with a new method called for example allSelection which make checked all my files mapped in the MediaCard Component, how can i achieve it?
This is the function you need:
const allSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
Related
In my project I have the component ExportSearchResultCSV. Inside this component the nested component CSVLink exports a CSV File.
const ExportSearchResultCSV = ({ ...props }) => {
const { results, filters, parseResults, justify = 'justify-end', fileName = "schede_sicurezza" } = props;
const [newResults, setNewResults] = useState();
const [newFilters, setNewFilters] = useState();
const [data, setData] = useState([]);
const [isLoading, setIsLoading] = useState(true)
const [headers, setHeaders] = useState([])
const prepareResults = () => {
let newResults = [];
if (results.length > 1) {
results.map(item => {
newResults.push(parseResults(item));
}); return newResults;
}
}
const createData = () => {
let final = [];
newResults && newResults?.map((result, index) => {
let _item = {};
newFilters.forEach(filter => {
_item[filter.filter] = result[filter.filter];
});
final.push(_item);
});
return final;
}
console.log(createData())
const createHeaders = () => {
let headers = [];
newFilters && newFilters.forEach(item => {
headers.push({ label: item.header, key: item.filter })
});
return headers;
}
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters])
return (
<div className={`flex ${justify} h-10`} title={"Esporta come CSV"}>
{results.length > 0 &&
<CSVLink data={createData()}
headers={headers}
filename={fileName}
separator={";"}
onClick={async () => {
await setNewFilters(filters);
await setNewResults(prepareResults());
await setData(createData());
await setHeaders(createHeaders());
}}>
<RoundButton icon={<FaFileCsv size={23} />} onClick={() => { }} />
</CSVLink>}
</div >
)
}
export default ExportSearchResultCSV;
The problem I am facing is the CSV file which is empty. When I log createData() function the result is initially and empty object and then it gets filled with the data. The CSV is properly exported when I edit this component and the page is refreshed. I tried passing createData() instead of data to the onClick event but it didn't fix the problem. Why is createData() returning an empty object first? What am I missing?
You call console.log(createData()) in your functional component upon the very first render. And I assume, upon the very first render, newFilters is not containing anything yet, because you initialize it like so const [newFilters, setNewFilters] = useState();.
That is why your first result of createData() is an empty object(?). When you execute the onClick(), you also call await setNewFilters(filters); which fills newFilters and createData() can work with something.
You might be missunderstanding useEffect(). Passing something to React.useEffect() like you do
React.useEffect(() => {
setNewFilters(filters);
setNewResults(prepareResults());
setData(createData());
setHeaders(createHeaders());
}, [results, filters]) <-- look here
means that useEffect() is only called, when results or filters change. Thus, it gets no executed upon initial render.
I have a page with the following structure
const Upload = (props) => {
return (
<BaseLayout>
<ToolbarSelection />
<Box>
<FileDropArea />
</Box>
</BaseLayout>
)
}
I have a method which works in the component <FileDropArea />
This is the method used as example
const allSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
In React how can i call this method allSelection from the <ToolbarSelection /> component, where i have my simple button like <Button>All Selection</Button>
You need to use React Context like this:
//create a fileContext.js
const fileContext = React.createContext();
const useFileContext = () => React.useContext(fileContext);
const FileContextProvider = ({ children }) => {
const [files, setFiles] = useState([]);
const allSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
// if you have other methods which may change the files add them here
return (
<fileContext.Provider
value={{
files,
setFiles,
allSelection,
}}
>
{children}
</fileContext.Provider>
);
};
use fileContextProvider in your upload file
const Upload = (props) => {
return (
<FileContextProvider>
<BaseLayout>
<ToolbarSelection />
<Box>
<FileDropArea />
</Box>
</BaseLayout>
</FileContextProvider>
);
};
use it, for example in ToolbarSelection like this:
const ToolbarSelection = () => {
const {files, allSelection} = useFileContext();
// do other stuff
}
React Hooks
I assume you are looking to make the allSelection function reusable. Hooks are a great way to make logic reusable across components.
Create a custom hook useAllSelection. Note that hooks should have a use prefix.
const useAllSelection = (files) => {
const [files, setFiles] = useState([]);
const handleAllSelection = () => {
setFiles((files) =>
files.map((file) => {
file.checked = true;
return file;
})
);
};
return { handleAllSelection };
};
const ToolbarSelection = () => {
// import the hook and use
const { handleAllSelection } = useAllSelection();
return (
<button onClick={handleAllSelection}>All Selection</button>
)
}
ReactJS allows to perform this scenario in a different way. Let me explain it: if you press a button in the ToolbarSelection, pass the value of the new state of that button to FileDropArea as props. Then, in the FileDropArea render, call the method or not depending on the value of that property
const Upload = (props) => {
return (
<BaseLayout>
<ToolbarSelection
onSelectionClick={(value) => setSelected(value)}
/>
<Box>
<FileDropArea
selected = { /* state of a button in the Toolbar */}
/>
</Box>
</BaseLayout>
)
}
Note how the callback in the Toolbar changes the state, and how this new state is passed to FileDropArea as property
I have a set of card objects that I map over.
When I click on a card it adds the selected class which in turn gives it a border to show the user it is selected, it also adds the id of the card to the selectedCards useState array.
WHAT I WANT TO HAPPEN:
Each card object has a creditAvailable key state which is equal to a figure.
On selection (click) of the card, in addition to selecting the card I would also like to add up the creditAvailable and display it on the screen. and when I unselect the card I would like the figure to go down.
WHAT I HAVE TRIED:
I thought it would be as simple as calling the function to add up the credit inside the first function which selects the card, however when console logging inside the first function I see that the state has not yet updated. (scope).
I then tried to call the function outside of the first function but it gave me an infinite loop. Here is my code.
Any ideas? Thanks
const [cards, setCards] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const [total, setTotal] = useState();
const handleSelectCard = (id) => {
if (selectedCards.includes(id)) {
const filteredIds = selectedCards.filter((c) => c !== id);
setSelectedCards([...filteredIds]);
} else {
setSelectedCards([...selectedCards, id]);
}
// addUpCreditAvailable(); // nothing happens
console.log(selectedCards); // []
};
console.log(selectedCards) // [1] for example. This is in the global scope
const addUpCreditAvailable = () => {
console.log("inside add up credit");
const chosenCards = selectedCards.map((id) => {
const foundCard = allCards.find((card) => {
return card.id === id;
});
return foundCard;
});
const result = chosenCards.reduce((acc, card) => {
return acc + card.creditAvailable;
}, 0);
setTotal(result);
return result;
};
return (
<div className="Container">
<UserInputForm submitData={handleSubmitData} />
<h1> Cards available to you displayed are below!</h1>
{cards.map(
({
id,
name,
number,
apr,
balanceTransfer,
purchaseDuration,
creditAvailable,
expiry,
}) => (
<CreditCard
key={id}
name={name}
number={number}
apr={apr}
balanceTransferDuration={balanceTransfer}
purchaseOfferDuration={purchaseDuration}
creditAvailable={creditAvailable}
expiry={expiry}
onClickCard={() => handleSelectCard(id)}
selected={selectedCards.includes(id)}
/>
)
)}
<span> £{total}</span>
)}
I figured it out with the help from above. As Wilms said i had to return the result of the handleSelectCard function and return the result of the addUpCredit function. Then I called the addUpCreditAvailable with the selectedCards state and stored the result in a variable which i then displayed in my render method.
const [cards, setCards] = useState([]);
const [selectedCards, setSelectedCards] = useState([]);
const handleSelectCard = (id) => {
if (selectedCards.includes(id)) {
const filteredIds = selectedCards.filter((c) => c !== id);
setSelectedCards([...filteredIds]);
} else {
setSelectedCards([...selectedCards, id]);
}
return selectedCards;
};
const addUpCreditAvailable = (selectedCards) => {
const chosenCards = selectedCards.map((id) => {
const foundCard = allCards.find((card) => {
return card.id === id;
});
return foundCard;
});
const result = chosenCards.reduce((acc, card) => {
return acc + card.creditAvailable;
}, 0);
return result;
};
const totalCredit = addUpCreditAvailable(selectedCards);
render method:
render (
[...]
{selectedCards.length && (
<div className={bem(baseClass, "total-credit")}>
Total Credit available: £{totalCredit}
</div>
)}
[...]
)
Assuming I have a slow component, it makes sense to memoize it, e.g.
const SlowButton = ({onClick}) => {
// make some heat
const value = (function f(n) { return n < 2 ? Math.random() : f(n-1) + f(n-2)})(32)|0;
return <button onClick={() => onClick(value)}>{value}</button>
}
const MemoButton = React.memo(SlowButton);
If I use the MemoButton in a component like:
const Counter = () => {
const [clicks, setClicks ] = useState(0);
const handleClick = () => {
setClicks(clicks + 1);
}
return <div>
<div>{clicks}</div>
<MemoButton onClick={handleClick} />
</div>
}
Then the MemoButton still re-renders every time because the onClick property is a new function every render. It is pretty easy to resolve this with:
const Counter2 = () => {
const [clicks, setClicks] = useState(0);
const handleClick = useCallback(() => {
setClicks(c => c + 1);
},[]);
return <div>
<div>{clicks}</div>
<MemoButton onClick={handleClick} />
</div>
}
The above works fine, but with a more complicated component, it doesn't work so well:
const CounterGroup = () => {
const numButtons = 3;
const [message, setMessage] = useState('button X clicked');
const handleClick = (val, idx) => setMessage(`button ${idx} clicked: ${val}`);
return <div>
<div>{message}</div>
{Array(numButtons).fill(0).map((_, i) =>
<MemoButton key={i} onClick={(v) => handleClick(v,i)} />)
}
</div>
}
In the above code, (v) => handleClick(i,v) is always going to be a new function reference. Is there a good technique to keep this from changing every render?
One possibility is to just ignore changes to 'on...' props, but this just creates new problems:
const compareValuePropsOnly = (prev, next) => Object.entries(prev).every(
([k, v]) => k.substr(0, 2) === "on" || v === next[k]
);
const MemoOnlyValsButton = React.memo(SlowButton, compareValuePropsOnly);
Here is a codesandbox version:
https://codesandbox.io/s/memoization-function-reference-changes-9c1fy
One solution is to let your SlowButton pass the i value, instead of getting it from the loop, and memoize handleClick
const SlowButton = ({onClick, i}) => {
// make some heat
const value = (function f(n) { return n < 2 ? Math.random() : f(n-1) + f(n-2)})(32)|0;
return <button onClick={() => onClick(value, i)}>{value}</button>
}
const MemoButton = React.memo(SlowButton);
const CounterGroup = () => {
const numButtons = 3;
const [message, setMessage] = useState('button X clicked');
const handleClick = React.useCallback((val, idx) => setMessage(`button ${idx} clicked: ${val}`), []);
return <div>
<div>{message}</div>
{Array(numButtons).fill(0).map((_, i) =>
<MemoButton key={i} i={i} onClick={handleClick} />)
}
</div>
}
Another way is to exclude 'onClick' props in your React.memo (since it's an event handler, it shouldn't impact how the component looks like).
const MemoButton = React.memo(SlowButton, (props1, props2) => {
// assume that SlowButton have some props that affect it's UI
// we don't compare onClick because it won't affect UI
return props1.someUIProps === props2.someUIProps;
})
Or you can use the useEventCallback hook to memo your function. In this case, you need to make a component between CounterGroup and MemoButton
const useEventCallback = (callback) => {
// store latest callback
const ref = useRef(callback);
useEffect(() => ref.current = callback);
// memoize the callback to maintain its identity
return useCallback((...args) => ref.current(...args), []);
}
const FastButton = ({onClick} => {
// FastButton will be re-rendered multiple times, but since memoOnClick have same identity
// on sub sequence re-renders, MemoButton should not be re-rendered
const memoOnClick = useEventCallback(onClick);
return <MemoButton onClick={memoOnClick} />
});
const CounterGroup = () => {
const numButtons = 3;
const [message, setMessage] = useState('button X clicked');
const handleClick = (val, idx) => setMessage(`button ${idx} clicked: ${val}`);
return <div>
<div>{message}</div>
{Array(numButtons).fill(0).map((_, i) =>
<FastButton key={i} onClick={(v) => handleClick(v,i)} />)
}
</div>
}
It seems like the value that should be memoized is the function/its result, not the component. At least in your example, there is nothing specific to the component that is passed into that expensive function. Even if it did accept values from the prop, you could still memoize that function based on the values specific to it, rather than have it re-evaluated for every other prop change.
const SlowButton = ({onClick}) => {
// make some heat
const value = useMemo(
() => (function f(n) { return n < 2 ? Math.random() : f(n-1) + f(n-2)})(32)||0,
[]
);
return <button onClick={() => onClick(value)}>{value}</button>
}
I have a react component that renders a series of other components, each with their own checkbox.
There is a state hook called rulesToDownload which begins as an empty array and has ids added to / removed from it as checkboxes are checked / unchecked.
When the 'download' button is clicked, the rulesToDownload array is passed to a data function DownloadFundDataById that forEach's over the array and calls window.open for each value with an api call with the id appended. The data function is imported into the component, not passed in as a prop.
This causes multiple tabs to flash up before closing when the data downloads. It's not perfect but it works.
I want to complete my test coverage and need to test that the function gets called on button click, and that it does what it should.
Any help appreciated.
Code below:
Summary.test.js:
it(`should create correct download array when some rules are selected`, async () => {
global.open = sandbox.spy();
fetch.mockResponseOnce(JSON.stringify(selectedRules));
wrapper = mount(<Summary/>);
await act(async () => {} );
wrapper.update();
wrapper.find('ReportProgressSummary').first().find('input').last().simulate('change', {target: {checked: true}});
wrapper.find('button').first().simulate('click');
expect(global.open).to.have.been.called();
});
I can confirm that all the 'find' statements are correct, and correctly update the checked value.
Summary.js:
const Summary = () => {
const [expand, setExpand] = useState(false);
const [buttonText, setButtonText] = useState("expand other rules");
const [rulesToDownload, setRulesToDownload] = useState([]);
const [data, setData] = useState([]);
const [dataLoadComplete, setDataLoadComplete] = useState(false);
const [dataLoadFailed, setDataLoadFailed] = useState(false);
useEffect(() => {
loadData();
}, []);
const loadData = async () => {
try {
let importedData = await ExecuteRules();
setData(importedData);
setDataLoadComplete(true);
} catch (_) {
setDataLoadFailed(true);
}
};
const onButtonClick = () => {
setExpand(!expand);
if(!expand) setButtonText("hide other rules");
else setButtonText("expand other rules");
};
const modifyDownloadArray = (id, checked) => {
let tempArray;
if(checked) tempArray = [...rulesToDownload, id];
else tempArray = [...rulesToDownload.filter(ruleId => ruleId !== id)];
setRulesToDownload([...tempArray]);
};
const dataFilter = (inputData, isFavouriteValue) => {
return inputData.filter(rule => rule.isFavourite === isFavouriteValue)
.sort((a, b) => a.percentage - b.percentage)
.map((rule, i) => {
return <ReportProgressSummary
result={rule.percentage}
id={rule.id}
title={rule.name} key={i}
modifyDownloadArray={modifyDownloadArray}
/>
})
};
return (
<div className="test">
{
dataLoadFailed &&
<div>Rule load failed</div>
}
{
!dataLoadComplete &&
<LoadingSpinnerTitle holdingTitle="Loading rule data..."/>
}
{
dataLoadComplete &&
<Fragment>
<PageTitle title="System Overview"/>
<LineAndButtonContainerStyled>
<ContainerStyled>
{
dataFilter(data, true)
}
</ContainerStyled>
<ContainerStyled>
<ButtonStyled
disabled={!rulesToDownload.length}
onClick={() => DownloadFundDataById(rulesToDownload)}>
download
</ButtonStyled>
</ContainerStyled>
</LineAndButtonContainerStyled>
<LineBreakStyled/>
<ButtonStyled onClick={() => onButtonClick()}>{buttonText}</ButtonStyled>
{
expand &&
<ContainerStyled>
{
dataFilter(data, false)
}
</ContainerStyled>
}
</Fragment>
}
</div>
)
};
export default Summary;
DataMethod.js:
export function DownloadFundDataById(downloadArray) {
downloadArray.forEach(id => window.open(baseApiUrl + '/xxxx/xxxx/' + id));
}
I can confirm the url is fine, just replaced for now
TestSetup:
const doc = jsdom.jsdom('<!doctype html><html><body></body></html>')
global.document = doc;
global.window = doc.defaultView;
configure({ adapter: new Adapter() });
global.expect = expect;
global.sandbox = sinon.createSandbox();
global.React = React;
global.mount = mount;
global.shallow = shallow;
global.render = render;
global.fetch = jestFetchMock;
global.act = act;
chai.use(chaiAsPromised);
chai.use(sinonChai);
chai.use(chaiEnzyme());
chai.use(chaiJestDiff());
console.error = () => {};
console.warn = () => {};
Current test output says that global.open is not being called. I know this makes sense as it isn't actually assigned as a prop to the onClick of the button or anything. This I think is one of my issues - I can't assign a stub to the button directly, but I'm trying not to re-write my code to fit my tests...
Managed to get this working with a couple of updates to my test file:
it(`should create correct download array when some rules are selected`, async () => {
global.open = sandbox.stub(window, "open");
fetch.mockResponseOnce(JSON.stringify(selectedRules));
wrapper = mount(<Summary/>);
await act(async () => {} );
wrapper.update();
wrapper.find('ReportProgressSummary').first().find('input').last().simulate('change', {target: {checked: true}});
wrapper.find('button').first().simulate('click');
expect(global.open).to.have.been.called;
});
the sandbox.spy() was updated to a sandbox.stub() with (window, "open")
thanks to this article for the help!
https://github.com/mrdulin/mocha-chai-sinon-codelab/blob/master/src/stackoverflow/53524524/index.spec.js
Also the expect statement using to.be.called() is actually not a function and so was updated to to.be.called