Reload table data when select menu item is changed - javascript

I have this table with select menu:
export interface IActivePairsProps extends StateProps, DispatchProps, RouteComponentProps<{ url: string }> {}
export const ActivePairs = (props: IActivePairsProps) => {
const [paginationState, setPaginationState] = useState(
overridePaginationStateWithQueryParams(getSortState(props.location, ITEMS_PER_PAGE, 'id'), props.location.search)
);
const [exchangeId, setExchangeId] = useState('');
const getAllEntities = () => {
props.getEntities(paginationState.activePage - 1, paginationState.itemsPerPage, `${paginationState.sort},${paginationState.order}`);
props.getExchangesList();
};
const sortEntities = () => {
getAllEntities();
const endURL = `?page=${paginationState.activePage}&sort=${paginationState.sort},${paginationState.order}&exchangeId=${exchangeId}`;
if (props.location.search !== endURL) {
props.history.push(`${props.location.pathname}${endURL}`);
}
};
useEffect(() => {
sortEntities();
}, [paginationState.activePage, paginationState.order, paginationState.sort]);
useEffect(() => {
const params = new URLSearchParams(props.location.search);
const page = params.get('page');
const sort = params.get('sort');
if (page && sort) {
const sortSplit = sort.split(',');
setPaginationState({
...paginationState,
activePage: +page,
sort: sortSplit[0],
order: sortSplit[1],
});
}
const exchangeId = params.get('exchangeId');
}, [props.location.search]);
const sort = p => () => {
setPaginationState({
...paginationState,
order: paginationState.order === 'asc' ? 'desc' : 'asc',
sort: p,
});
};
const handlePagination = currentPage =>
setPaginationState({
...paginationState,
activePage: currentPage,
});
const handleSyncList = () => {
sortEntities();
};
const { activePairsList, exchangesList, match, loading, totalItems } = props;
return (
<div>
<div className="table-responsive">
{activePairsList && activePairsList.length > 0 ? (
<Table responsive>
<thead>
<tr>
.....
<select onChange={e => setExchangeId(e.target.value)}>
{exchangesList
? exchangesList.map(otherEntity => (
<option value={otherEntity.exchangeId} key={otherEntity.exchangeId}>
{otherEntity.exchangeLongName} - {otherEntity.exchangeId}
</option>
))
: null}
</select>
.........
</Table>
) : (
!loading && <div className="alert alert-warning">No Active Pairs found</div>
)}
</div>
{props.totalItems ? (
<div className={activePairsList && activePairsList.length > 0 ? '' : 'd-none'}>
<Row className="justify-content-center">
<JhiItemCount page={paginationState.activePage} total={totalItems} itemsPerPage={paginationState.itemsPerPage} />
</Row>
<Row className="justify-content-center">
<JhiPagination
activePage={paginationState.activePage}
onSelect={handlePagination}
maxButtons={5}
itemsPerPage={paginationState.itemsPerPage}
totalItems={props.totalItems}
/>
</Row>
</div>
) : (
''
)}
</div>
);
};
const mapStateToProps = ({ activePairs, exchangesList }: IRootState) => ({
activePairsList: activePairs.entities,
exchangesList: exchangesList.entities,
loading: activePairs.loading,
totalItems: activePairs.totalItems,
});
const mapDispatchToProps = {
getEntities,
getExchangesList,
};
type StateProps = ReturnType<typeof mapStateToProps>;
type DispatchProps = typeof mapDispatchToProps;
export default connect(mapStateToProps, mapDispatchToProps)(ActivePairs);
How I can reload the table data when I change the select menu item? I would like to reload the data from the table data with the new selected exchageId param.

useEffect(fn, deps);
As we can see in the React documentation, the way we use the effect hook looks like this:
,fn is the effectful function, and deps is an array of values it depends on. Every time the component renders, React checks if all the values in the deps array are still the same. If any of them has changed since the last render, fn is run again.,All right, so far all the examples exhibit the same behavior. The effect simply doesn't run again if the dependency value doesn't change.
So you only need to give the useEffect hook exchageId as deps and the component's loading function as fn, then UseEffect will rerenders your component.

Related

useMemo does not rerender dynamic controls

I have list of controls which are sent by backend. Sometimes it is necessary to update options of dropdown control.
I thought that it would work. However, memoizedControls is not rerendered.
I believe that it should work like this:
press the button and handleFieldsChange() function is triggered.
then setTechSpec('') sets techSpec to ''
then custom hook usePerson is triggered because it has techSpec in its dependency array
then memoizedControls is triggered because it has personList in its dependency array
and updateControlOptions() updates options of controls
However, UI does not have rerendered and new options of personList is not rerendered.
const [techSpec, setTechSpec] = useState('1')
const [personList, isLoadingPersonList] = usePerson(techSpec)
const handleFieldsChange = (changedValue: EditablePersonValues) => {
setTechSpec('')
fetchPerson()}
const updateControlOptions = (
controls: FormControl[],
controlName: string,
newOptions: SelectOption[],
) =>
controls.map((control) =>
control.name === controlName
? { ...control, options: newOptions }
: { ...control },)
const memoizedControls = useMemo(() => {
console.log('memoizedControls')
if (personList.length > 0)
return updateControlOptions(
controls,
'personId',
personList,
)
return controls
}, [controls, personList])
const fetchPerson = () => {
const localTechSpecification = form.getFieldValue('techSpecification')
setTechSpec(localTechSpecification)
form.setFieldsValue({ personId: undefined })
}
and:
return (
{memoizedControls.map(
({ name, type, displayName, required, options, measure }) => {
return (
<MyDynamicField
key={name}
name={name}
controlType={type}
displayName={`${displayName}${measure ? `, ${measure}` : ''}`}
required={required}
value={value}
itemOptions={options}
/>
)
},
)}
)
My question is that "usePerson" hook is being re-executed when the "techSpec" state value changes. personList is updated. But memoizedControls does not show new values of personList. Maybe do you know the reason of why memoizedControls is not rerendered?
Please, does anybody know what I am doing wrong?
import React, { useState, useMemo } from 'react';
const MyComponent = ({ personList }) => {
const [techSpec, setTechSpec] = useState('1');
const [isLoadingPersonList, setIsLoadingPersonList] = useState(false);
const handleFieldsChange = (changedValue) => {
setTechSpec('');
fetchPerson();
};
const updateControlOptions = (controls, controlName, newOptions) =>
controls.map((control) =>
control.name === controlName
? { ...control, options: newOptions }
: { ...control }
);
const memoizedControls = useMemo(() => {
console.log('memoizedControls');
if (personList.length > 0) {
return updateControlOptions(controls, 'personId', personList);
}
return controls;
}, [controls, personList]); // personList is included in the dependency array
const fetchPerson = () => {
const localTechSpecification = form.getFieldValue('techSpecification');
setTechSpec(localTechSpecification);
form.setFieldsValue({ personId: undefined });
};
return (
<div>
{memoizedControls.map(({ name, type, displayName, required, options, measure }) => (
<DynamicField
key={name}
name={name}
controlType={type}
displayName={`${displayName}${measure ? `, ${measure}` : ''}`}
required={required}
value={value}
itemOptions={options}
/>
))}
</div>
);
};
The above code was fine, the problem was in component <MyDynamicField/>.
So itemOptions should be added in dependency array [height, width, itemOptions]) of useEffect. So code would look like this:
export const MyDynamicField = ({
name,
controlType,
displayName,
required,
value,
itemOptions,
moulds,
initData,
height,
width,
}: Props) => {
const [options, setOptions] = useState<SelectOption[]>([])
useEffect(() => {
const prepareOptions = filterOptions(
name,
moulds,
initData,
itemOptions,
height,
width,
)
setOptions(prepareOptions)
}, [height, width, itemOptions])
return (
<ControlFactory
key={name}
name={name}
controlType={controlType}
displayName={displayName}
required={required}
options={options}
value={value}
/>
)
}
and then useEffect and useState can be removed and useMemo can be used:
export const MyDynamicField = ({
name,
controlType,
displayName,
required,
value,
itemOptions,
moulds,
initData,
height,
width,
}: Props) => {
const options = useMemo(
() => filterOptions(name, moulds, initData, itemOptions, height, width),
[name, moulds, initData, itemOptions, height, width],
)
return (
<ControlFactory
key={name}
name={name}
controlType={controlType}
displayName={displayName}
required={required}
options={options}
value={value}
/>
)
}

useReducer passed with useContext but a child component displays empty state.map

First, I would thank you for the support.
As new to the ReactJS world, I am trying to complete a concept example of a product store with some filters as checkbox. The idea is that you select a filter, you get displayed the products that have the selected proprety.
Everything works, except that when you refresh the page you get the filters column and a blank column where products are supposed to appear, even if the console.log(state) give back the correct array of objects.
As you click a checkbox (the filters) it render correctly and the products appear.
The GITHUB LINK for the complete code.
Here the component CardProduct that does not display at refresh.
import React, { useContext, useEffect } from 'react'
import { AppContext } from '../../App'
import { Hearty } from '../Hearty'
import Star from '../Star'
import boiler from '../../images/boiler.png'
import Confronta from '../Confronta'
const CardProduct = ({ count, setCount }) => {
const [state, dispatch] = useContext(AppContext)
console.log('State in CardProduct:', state)
function returnCardProduct () {
return (
state.map((item, i) => {
const { brand, descrizione, prezzo, note, stelle } = item
return (
<div className="row">
<div className="colcard">
<div key={ i } className="card"
style={ { width: 'auto', height: 'auto' } }>
<Hearty/>
<img className="card-img-top" src={ boiler } alt="boiler"/>
<div className="card-body">
<p className="card-title"> { brand.toUpperCase() }</p>
<h6 className="card-text">{ descrizione }</h6>
<Star stelle={ stelle }/>
<h4> { prezzo } </h4>
<h5> { note } </h5>
<Confronta count={ count } setCount={ setCount }/>
</div>
</div>
</div>
</div>
)
}))
}
return (
<div className="container">
{ returnCardProduct() }
</div>
)
}
export default CardProduct
Here the Filters component
import { useContext, useEffect, useState } from 'react'
import { AppContext } from '../App'
const Filters = () => {
const [stock, setStock] = useState([])
const [state,dispatch] = useContext(AppContext)
function fetchInitialStock () {
async function fetchStock () {
let result1 = await fetch('http://localhost:9000/stock').
then(result1 => result1.json()).
then(data => setStock(data))
}
fetchStock()
return stock
}
useEffect (()=>fetchInitialStock(),[])
console.log( 'initStock' ,stock)
return (
<>
<div className="container">
<div className="row">
<div className="categories">
<p>CATEGORIE</p>
<h6>Riscaldamento</h6>
<h6>Casa e acqua</h6>
<h6>Casa</h6>
<h6>Acqua</h6>
</div>
<div className="scegli">
<p>SCEGLI PER</p>
<h6><span><input type="checkbox"
name="DISPONIBILI"
onChange={(e)=> {
e.target.checked ? dispatch({ type: 'DISPONIBILI' }) : dispatch({ type:'PREV' })
} }/>
</span> Disponibili ({stock.map((item) => item.disponibili )}) </h6>
<h6><span><input type="checkbox"
name="PROMO"
onChange={(e)=> e.target.checked ? dispatch({ type: 'PROMO' }) : dispatch({ type: 'PREV' }) }
/> </span>In Promozione ({ stock.map((item) => item.inSconto) }) </h6><br/>
</div>
<div className="marche">
<p>MARCHE</p>
<h6><span><input type="checkbox" name="ariston" onChange={(e)=>{
e.target.checked
? dispatch({ type: 'ARISTON' })
: dispatch({ type: 'PREV' })
}}
/> </span> Ariston ({stock.map((item)=>item.hasOwnProperty('brand')? item.brand.ariston: null)})</h6>
<h6><span><input type="checkbox" name="baxi" onChange={(e)=>{
e.target.checked
? dispatch({ type: 'BAXI' })
: dispatch({ type: 'PREV' })
}}/> </span>Baxi ({stock.map((item)=>item.hasOwnProperty('brand')? item.brand.baxi : null)})</h6><br/>
</div>
<div className="tipologia">
<p>TIPOLOGIA</p>
<h6><span><input type="checkbox" name="condensazione" onChange={(e)=>{
e.target.checked
? dispatch({ type: 'CONDENSAZIONE' })
: dispatch({ type: 'PREV' })
}}/> </span> A Condensazione ({stock.map((item)=>item.hasOwnProperty('tipologia')? item.tipologia.condensazione: null)}) </h6>
<h6><span><input type="checkbox" name="cameraAperta" onChange={(e)=>{
e.target.checked
? dispatch({ type: 'APERTA' })
: dispatch({ type: 'PREV' })
}}/> </span>Camera Aperta ({ stock.map((item)=>item.hasOwnProperty('tipologia')? item.tipologia.cameraAperta: null) }) </h6>
<h6><span><input type="checkbox" name="cameraStagna" onChange={(e)=>{
e.target.checked
? dispatch({ type: 'STAGNA' })
: dispatch({ type: 'PREV' })
}}/> </span>Camera Stagna ({ stock.map((item)=>item.hasOwnProperty('tipologia')? item.tipologia.cameraStagna: null) })</h6><br/>
</div>
</div>
</div>
</>
)
}
export default Filters
..and FINALLY the App()
import CardProduct from './components/CardProduct'
import { createContext, useReducer, useState, useEffect } from 'react'
import Filters from './components/Filters'
import Footer from './components/Footer/Footer'
export const AppContext = createContext()
function App () {
const [count, setCount] = useState(0)
function setInit (data, array) {
data.map((item) => array.push(item))
return array
}
/*Function for setting BOILERS from fetch*/
function fetchInitialBoiler () {
let initB = []
async function fetchBoilers () {
let response = await fetch('http://localhost:9000/boilers').
then(response => response.json()).
then(data => setInit(data, initB))
}
fetchBoilers()
return initB
}
const initBoilers = fetchInitialBoiler()
const [prev, setPrev] = useState([])
const [state, dispatch] = useReducer(reducer, initBoilers)
/* Define the reducer function*/
function reducer (state, action) {
let current
switch (action.type) {
case 'DISPONIBILI':
current = []
current = state.filter((item) => item.disponibile ? item : null)
setPrev(current)
return current
case 'PROMO':
current = []
current = state.filter((item) => item.inSconto ? item : null)
setPrev(current)
return current
case 'ARISTON':
current = []
current = state.filter(
(item) => ((item.hasOwnProperty('brand')) &&
(item.brand === 'Ariston'))
? item
: null)
setPrev(current)
return current
case 'BAXI':
current = []
current = state.filter(
(item) => (item.hasOwnProperty('brand')) && (item.brand === 'Baxi')
? item
: null)
setPrev(current)
return current
case 'CONDENSAZIONE':
current = []
current = state.filter((item) => (item.hasOwnProperty('tipologia')) &&
(item.tipologia === 'condensazione')
? item
: null)
setPrev(current)
return current
case 'APERTA':
current = []
current = state.filter((item) => (item.hasOwnProperty('tipologia')) &&
(item.tipologia === 'camera-aperta')
? item
: null)
setPrev(current)
return current
case 'STAGNA':
current = []
current = state.filter((item) => (item.hasOwnProperty('tipologia')) &&
(item.tipologia === 'camera-stagna')
? item
: null)
setPrev(current)
return current
case 'PREV':
current = []
/*console.log('PREV', prev)*/
return prev
default:
return state
}
}
return (
<>
<AppContext.Provider value={ [state, dispatch] }>
<main>
<div className="container">
<div className="container">
<>
<div className="col align-self-start">
<Filters/>
</div>
<div className="col-9">
<CardProduct count={ count } setCount={ setCount }/>
</div>
</>
</div>
</div>
<>
<div>
<Footer className="footer" count={ count }/>
</div>
</>
</main>
</AppContext.Provider>
</>
)
}
export default App
--- THANK YOU ---
Your initB is an array declartion which doesn't re-render a React component. Replace it with useState to see the updated data after the API call.
const [init, setInit] = useState([]);
Secondly,
const initBoilers = fetchInitialBoiler()
you should invoke the fetchInitialBoiler() after the document is mounted, with the current approach chances are the API is invoked even before the intial mount.
Invoke it in an [useEffect][1] block.
useEffect(()=>{
fetchInitialBoiler() //
// simply invoke it, since you're not returning anything from the function, there's no need to save the response.
}, [])
Your function should set the state in the then/catch block, so that the component tree re-renders in an error case.
async function fetchBoilers () {
let response = await fetch('http://localhost:9000/boilers').
then(response => response.json())
.then(data => setInit(response))
.catch(error => setError(error); // state
}
More on Fetch API: https://developer.mozilla.org/en-US/docs/Web/API/Fetch_API
So in the end the problem was to pass the fetch as initialState to useReducer. Searching online I found that is passed throught a dispatch action inside useEffect hook. In this case the useReducer hook is declared with initialState '[]' inside the component. Here the code:
const CardProduct = ({ count, setCount }) => {
const [state, dispatch] = useReducer(reducer, [])
async function initfetch () {
let response = await fetch('http://localhost:9000/boilers').
then(response => response.json()).
then(data => dispatch({
type : 'INITIALIZE',
data: data
}))}
useEffect(() => {
initfetch()
}, [])

2 dropdown select option iterating through same array but should not be able to select same selected value (ReactJS)

Currently I am trying to build 2 dropdown select option component that only shows columns, but the point is that it should not display the already selected value in other dropdown select option. So each value can only be selected once. So how can I fix that in the code.
Click here to go to Codesandbox
Parent component
return (
<div>
<TableDropdownFilter
columns={columns}
onSelect={setHiddenCol1}
/>
<TableDropdownFilter
columns={columns}
onSelect={setHiddenCol2}
/>
<Table
columns={columns}
data={data}
hiddenColumnsOne={hiddenCol1}
hiddenColumnsTwo={hiddenCol2}
/>
</div>
)
Child component
interface TableDropdownProps {
columns: any;
onSelect: any;
firstFilter?: string;
secondFilter?: string;
}
export const TableDropdownFilter: FC<TableDropdownProps> = ({
columns,
onSelect,
firstFilter,
secondFilter
}) => {
const [columnShow, setColumnShow] = useState<string>("");
const [openDropdown, setOpenDropdown] = useState(false);
const wrapperRef = useRef(null);
const useOutsideAlerter = (ref: any) => {
useEffect(() => {
const handleClickOutside = (event: MouseEvent) => {
if (ref.current && !ref.current.contains(event.target)) {
setOpenDropdown(false)
}
}
document.addEventListener("mousedown", handleClickOutside);
return () => {
document.removeEventListener("mousedown", handleClickOutside);
};
}, [ref]);
}
useOutsideAlerter(wrapperRef)
const dropdownFilter = (selectedColumn: string) => {
setColumnShow(selectedColumn);
setOpenDropdown(false);
onSelect(selectedColumn);
}
const toggleDropdownOpen = () => setOpenDropdown(!openDropdown);
console.log(firstFilter, secondFilter);
return (
<TableFilter>
<TableFilterBlock ref={wrapperRef}>
<TableFilterInput onClick={() => toggleDropdownOpen()}>
{columnShow.length > 0 ? columnShow : "Select"}</TableFilterInput>
{typeof columnShow}
<TableFilterDropdown toggleDropdown={openDropdown}>
{columns.slice(1).map((item: any, index: number) => (
<TableDropdownList key={index}>
<div onClick={() => dropdownFilter(item.id)}>
{item.id}
</div>
</TableDropdownList>
))}
</TableFilterDropdown>
</TableFilterBlock>
</TableFilter>
)
}
You only need to pass a single value to filter by to the TableDropdownFilter component. Use the passed filter prop to do an inline filtering of the columns options.
TableDropdownFilter
interface TableDropdownProps {
columns: any;
onSelect: any;
filter?: string; // <-- single filter by value
}
export const TableDropdownFilter: FC<TableDropdownProps> = ({
columns,
onSelect,
filter // <-- destructure filter prop
}) => {
const [columnShow, setColumnShow] = useState<string>("");
const [openDropdown, setOpenDropdown] = useState(false);
const wrapperRef = useRef(null);
...
return (
<TableFilter>
<TableFilterBlock ref={wrapperRef}>
<TableFilterInput onClick={() => toggleDropdownOpen()}>
{columnShow.length > 0 ? columnShow : "Select"}
</TableFilterInput>
{typeof columnShow}
<TableFilterDropdown toggleDropdown={openDropdown}>
{columns
.slice(1)
.filter((item: any) => item.id !== filter) // <-- filter by item is
.map((item: any, index: number) => (
<TableDropdownList key={index}>
<div onClick={() => dropdownFilter(item.id)}>{item.id}</div>
</TableDropdownList>
))}
</TableFilterDropdown>
</TableFilterBlock>
</TableFilter>
);
};
TableComponent
Pass the appropriate filter value for the "other" select input.
export const TableComponent: FC<TableComponentProps> = ({ columns }) => {
const [hiddenCol1, setHiddenCol1] = useState<string>("");
const [hiddenCol2, setHiddenCol2] = useState<string>("");
const data = useMemo(() => TableContent, []);
return (
<div>
<TableDropdownFilter
columns={columns}
onSelect={setHiddenCol1}
filter={hiddenCol2} // <-- filter by dropdown 2 value
/>
<TableDropdownFilter
columns={columns}
onSelect={setHiddenCol2}
filter={hiddenCol1} // <-- filter by dropdown 1 value
/>
<Table
columns={columns}
data={data}
hiddenColumnsOne={hiddenCol1}
hiddenColumnsTwo={hiddenCol2}
/>
</div>
);
};

Pass data from API to another component with TypeScript and ReactJS

i'am learning TS yet and I trying to create an application where I get data from API, show results and if someone click on item, it shows a modal with more details, but i'am trouble cause basically my component doesn't render... Look at my code =) !
import IMovie from "../../models/movie.model";
import Modal from "../modal/Modal";
import "./style";
import {
ResultsBody,
ResultsContainer,
TitleResult,
MovieStats,
MovieCover,
MovieStatsDescription,
} from "./style";
interface ISearch {
search?: string;
}
const URL =
"#";
const Results = ({ search }: ISearch) => {
const [data, setData] = React.useState<IMovie[]>([]);
const [currentPage, setCurrentPage] = React.useState(1);
const [dataPerPage] = React.useState(10);
async function getData() {
const response: AxiosResponse<any> = await axios.get(URL);
setData(response.data.results);
}
React.useEffect(() => {
getData();
}, []);
const indexLastData = currentPage * dataPerPage;
const indexFirstData = indexLastData - dataPerPage;
const currentData = data.slice(indexFirstData, indexLastData);
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
const filteredData = data.filter((results) => {
return results.title.toLowerCase().includes(search!.toLocaleLowerCase());
});
return (
<>
<ResultsContainer>
<TitleResult>
<span>Personagem</span>
<span>Sinopse</span>
<span>Data</span>
</TitleResult>
{!search
? currentData.map((item) => (
<ResultsBody
key={item.id}
// onClick={() => {
// selectedMovie(item);
// }}
>
<MovieCover
src={`https://image.tmdb.org/t/p/w185${item.poster_path}`}
alt="poster"
/>
<MovieStats style={{ fontWeight: `bold` }}>
{item.title}
</MovieStats>
<MovieStatsDescription>{item.overview}</MovieStatsDescription>
<MovieStats>{item.release_date}</MovieStats>
</ResultsBody>
))
: filteredData.map((item) => (
<ResultsBody key={item.id}>
<MovieCover
src={`https://image.tmdb.org/t/p/w185${item.poster_path}`}
alt="poster"
/>
<MovieStats style={{ fontWeight: `bold` }}>
{item.title}
</MovieStats>
<MovieStatsDescription>{item.overview}</MovieStatsDescription>
<MovieStats>{item.release_date}</MovieStats>
</ResultsBody>
))}
</ResultsContainer>
<Modal data={data} /> //HERE IS WHERE I'AM CALLING MY MODAL, I want to pass data here
<Pagination
dataPerPage={dataPerPage}
totalData={data.length}
paginate={paginate}
currentPage={currentPage}
/>
</>
);
};
export default Results;
This is my MODAL component
import React from "react";
import { ModalContainer } from "./style";
import IMovie from "../../models/movie.model";
interface IData {
data: IMovie[];
}
const Modal = ({ data }: IData) => {
console.log(data);
return <ModalContainer>{data.title}</ModalContainer>; //HERE IS NOT WORKING
};
export default Modal;
As you can see guys, I can show all results on console.log, but when I put inside the return the log says ''TypeError: Cannot read property 'title' of undefined''
If someone could help me I'd really appreciate! Thanks a lot =)
Movie vs Array
You are getting the error
'Property 'title' does not exist on type 'IMovie[]'. TS2339
in your Modal component because data is an array of movies. An array doesn't have a title property.
You want the modal to show one movie, so you should only pass it one movie.
interface IData {
data: IMovie;
}
Current Selection
Changing the IData interface fixes the issues in Modal, but creates a new error in Results because we are still passing an array. The correct prop is the data for the movie that was clicked. What movie is that? We need to use a useState hook in order to store that data.
Depending on where you control the open/closed state of the Modal, you may also want to pass an onClose callback that clears the selected movie state.
the state:
const [selected, setSelected] = React.useState<IMovie | null>(null); // is a movie or null
in the movie:
onClick={() => setSelected(item)}
the modal:
{selected === null || (
<Modal
data={selected}
onClose={() => setSelected(null)}
/>
)}
Avoid Duplicated Code Blocks
You are rendering a movie the same way whether it's from currentData or filteredData, so we want to combine those. We could create a shared renderMovie callback or ResultsMovie component to use in both loops, but I think we can actually handle it higher up and just have one loop.
You also want your pagination to reflect the pages of just the matching movies when we are filtering based on a search.
// the matchingMovies is a filtered array when there is a search, or otherwise the entire array
const matchingMovies = search
? data.filter((result) =>
result.title.toLowerCase().includes(search.toLowerCase())
)
: data;
const indexLastData = currentPage * dataPerPage;
const indexFirstData = indexLastData - dataPerPage;
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// total for the pagination should be based on matchingMovies instead of data
const totalData = matchingMovies.length;
// make the currentData from the matchingMovies
const currentData = matchingMovies.slice(indexFirstData, indexLastData);
There might be some bugs or potential additional improvements but I can't actually run this without your components :)
const Results = ({ search }: ISearch) => {
const [data, setData] = React.useState<IMovie[]>([]);
const [currentPage, setCurrentPage] = React.useState(1);
const [dataPerPage] = React.useState(10);
const [selected, setSelected] = React.useState<IMovie | null>(null); // is a movie or null
async function getData() {
const response: AxiosResponse<any> = await axios.get(URL);
setData(response.data.results);
}
React.useEffect(() => {
getData();
}, []);
// the matchingMovies is a filtered array when there is a search, or otherwise the entire array
const matchingMovies = search
? data.filter((result) =>
result.title.toLowerCase().includes(search.toLowerCase())
)
: data;
const indexLastData = currentPage * dataPerPage;
const indexFirstData = indexLastData - dataPerPage;
const paginate = (pageNumber: number) => setCurrentPage(pageNumber);
// make the currentData from the matchingMovies
const currentData = matchingMovies.slice(indexFirstData, indexLastData);
return (
<>
<ResultsContainer>
<TitleResult>
<span>Personagem</span>
<span>Sinopse</span>
<span>Data</span>
</TitleResult>
{currentData.map((item) => (
<ResultsBody key={item.id} onClick={() => setSelected(item)}>
<MovieCover
src={`https://image.tmdb.org/t/p/w185${item.poster_path}`}
alt="poster"
/>
<MovieStats style={{ fontWeight: `bold` }}>{item.title}</MovieStats>
<MovieStatsDescription>{item.overview}</MovieStatsDescription>
<MovieStats>{item.release_date}</MovieStats>
</ResultsBody>
))}
</ResultsContainer>
{selected === null || (
<Modal data={selected} onClose={() => setSelected(null)} />
)}
<Pagination
dataPerPage={dataPerPage}
totalData={matchingMovies.length}
paginate={paginate}
currentPage={currentPage}
/>
</>
);
};
interface ModalProps {
data: IMovie;
onClose: () => void;
}
const Modal = ({ data, onClose }: ModalProps) => {
console.log(data);
return <ModalContainer>{data.title}</ModalContainer>;
};

How to get the number of checked checkboxes in React.js?

I started learning React not so long ago. Decided to make some kind of "life checklist" as one of my beginner projects. I have been using Functional Components in the core.
FYI:
I have data.js as an array of objects where "action", "emoji" and unique ID are stored.
I import it into my App.js.
const App = () => {
//Looping over data
const items = data.map((item) => {
return (
<ChecklistItem action={item.action} emoji={item.emoji} key={item.id} />
);
});
return (
<>
<GlobalStyle />
<StyledHeading>Life Checklist</StyledHeading>
<StyledApp>{items}</StyledApp>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
Here is my <ChecklistItem/> component:
const ChecklistItem = ({ action, emoji }) => {
//State
const [isActive, setIsActive] = useState(false);
//Event Handlers
const changeHandler = () => {
setIsActive(!isActive);
};
return (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
);
};
export default ChecklistItem;
I would be satisfied with the functionality so far, but I need to show how many "active" checklist items were chosen in the parent <App/> component like "You have chosen X items out of {data.length}. How can I achieve this?
I assume that I need to lift the state up, but cannot understand how to implement this properly yet.
You can do that by simply creating a state for storing this particular count of active items.
To do that, you would need to update your <App/> component to something like this
const App = () => {
const [activeItemsCount, setActiveItemsCount] = useState(0);
//Looping over data
const items = data.map((item, index) => {
return (
<ChecklistItem
key={index}
action={item.action}
emoji={item.emoji}
setActiveItemsCount={setActiveItemsCount}
/>
);
});
return (
<>
<h1>Life Checklist</h1>
<div>{items}</div>
<div>Active {activeItemsCount} </div>
<h2>Overall number: {data.length}</h2>
</>
);
};
export default App;
And then in your <ChecklistItem /> component, you would need to accept that setActiveItemsCount function so that you can change the state of the activeItemsCount.
import React, { useState, useEffect } from "react";
const ChecklistItem = ({ action, emoji, setActiveItemsCount }) => {
const [isActive, setIsActive] = useState(false);
const changeHandler = () => {
setIsActive(!isActive);
};
useEffect(() => {
if (!isActive) {
setActiveItemsCount((prevCount) => {
if (prevCount !== 0) {
return prevCount - 1;
}
return prevCount;
});
}
if (isActive) {
setActiveItemsCount((prevCount) => prevCount + 1);
}
}, [isActive, setActiveItemsCount]);
return <input type="checkbox" checked={isActive} onChange={changeHandler} />;
};
export default ChecklistItem;
By using the useEffect and the checks for isActive and 0 value, you can nicely increment or decrement the active count number by pressing the checkboxes.
How about this?
const data = [
{ action: '1', emoji: '1', id: 1 },
{ action: '2', emoji: '2', id: 2 },
{ action: '3', emoji: '3', id: 3 },
];
const ChecklistItem = ({ action, emoji, isActive, changeHandler }) => {
return (
<div isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={changeHandler} />
<div>{emoji}</div>
<div>{action}</div>
</div>
);
};
const PageContainer = () => {
const [checkedItemIds, setCheckedItemIds] = useState([]);
function changeHandler(itemId) {
if (checkedItemIds.indexOf(itemId) > -1) {
setCheckedItemIds((prev) => prev.filter((i) => i !== itemId));
} else {
setCheckedItemIds((prev) => [...prev, itemId]);
}
}
const items = data.map((item) => {
const isActive = checkedItemIds.indexOf(item.id) > -1;
return (
<ChecklistItem
isActive={isActive}
changeHandler={() => changeHandler(item.id)}
action={item.action}
emoji={item.emoji}
key={item.id}
/>
);
});
return (
<div className="bg-gray-100">
<div>{items}</div>
<h2>
You have chosen {checkedItemIds.length} items out of {data.length}
</h2>
</div>
);
};
When data is used by a child component, but the parent needs to be aware of it for various reasons, that should be state in the parent component. That state is then handed to the child as props.
One way to do this would be to initialize your parent component with a piece of state that was an array of boolean values all initialized to false. Map that state into the checkbox components themselves and hand isActive as a prop based on that boolean value. You should then also hand the children a function of the parent that will change the state of the boolean value at a certain index of that array.
Here's a bit of a contrived example:
// Parent.tsx
const [checkBoxes, setCheckboxes] = useState(data.map(data => ({
id: data.id,
action: data.action,
emoji: data.emoji
isActive: false,
})));
const handleCheckedChange = (i) => {
setCheckboxes(checkBoxes => {
checkBoxes[i].isActive = !checkBoxes[i].isActive;
return checkBoxes;
})
}
return(
checkBoxes.map((item, i) =>
<ChecklistItem
action={item.action}
emoji={item.emoji}
key={item.id}
index={i}
isActive={item.isActive}
handleChange={handleCheckedChange}
/>
)
);
// CheckListItem.tsx
const CheckListItem = ({ action, emoji, index, isActive, handleChange }) => (
<StyledChecklistItem isActive={isActive}>
<input type="checkbox" checked={isActive} onChange={() => handleChange(index)} />
<StyledEmoji role="img">{emoji}</StyledEmoji>
<StyledCaption>{action}</StyledCaption>
</StyledChecklistItem>
)

Categories