I am building an where it has few dropdowns. Here the problem is when user make selection in dropdown and if they refresh dropdown selection value reset back to default value. Instead I want to keep the value persisted so that when user make refresh it gets value from last selected dropdown value.
For that i tried using localstorage and it does save the value. But inside useEffect I try to get value and pass it to make api call it gives empty string for few times. Since the server gives me error.
Here is my code
const DispensingIncidents = (props) => {
const classes = useStyles();
const {
getFilterData,
dispensingData, // data from server side
getOverviewData,
location,
history,
getAnalysis,
clearAnalysis,
getDuration,
} = props;
const [timeSpan, setTimeSpan] = React.useState("");
const [year, setYear] = React.useState(2020);
const [tabValue, setTabValue] = React.useState(0);
const [spanData, setSpanData] = React.useState([]);
const [dataType, setDataType] = React.useState("");
const [durationLabel, setDurationLabel] = React.useState("");
const [dataTo, setDataTo] = React.useState("");
const [dataFrom, setDataFrom] = React.useState("");
// incidencesSection hide and show
const [incidencesSection, setIncidencesSection] = React.useState(false);
// handle overview select state
const [overViewSelect, setOverViewSelect] = React.useState("");
// stay selected tab on refresh
// eslint-disable-next-line
const [selectTab, setSelectTab] = React.useState(0);
const {
loading,
duration,
period,
type,
_reference,
dispensingOverviewData,
overviewDataLoading,
incidenceAnalysisData, // bottom accordion data,
incidenceAnalysisDataArray, // bottom accordion data array
analysisDataLoading,
} = dispensingData;
const { count } = dispensingOverviewData;
useEffect(() => {
history.replace({
pathname: location.pathname,
search: `?year=${year}&period=${timeSpan}&type=${dataType}&duration=${durationLabel}&overview=${overViewSelect}`,
});
setYear(year);
setTimeSpan(timeSpan);
window.localStorage.setItem(
"incidenceState",
JSON.stringify({
year,
timeSpan,
dataType,
durationLabel,
dataTo,
dataFrom,
})
);
return () => {
window.localStorage.removeItem("incidenceState");
};
// eslint-disable-next-line
}, [
year,
timeSpan,
dataType,
durationLabel,
overViewSelect,
dataTo,
dataFrom,
]);
/**
* This updates on Year given
*/
useEffect(() => {
getFilterData(year);
getDuration(year);
}, [getFilterData, year, getDuration]);
const [once, setOnce] = React.useState(0);
useEffect(() => {
if (duration !== "" && type !== "") {
if (
type !== "" &&
once === 0 &&
duration.monthly.length > 0 &&
_reference !== ""
) {
setOnce(1);
console.log("OverviewPeriod", Object.keys(period));
getOverviewData(
Object.keys(period)[3],
duration.monthly[0].period.to,
duration.monthly[0].period.from,
Object.keys(type)[0],
_reference
);
setTimeSpan(Object.keys(period)[3]);
setDataFrom(duration.monthly[0].period.from);
setDataTo(duration.monthly[0].period.to);
setDataType(Object.keys(type)[0]);
}
}
}, [duration, type]);
/**
* GET query from url search param
* #usage query.get("year")
*/
function useQuery() {
return new URLSearchParams(location.search);
}
const query = useQuery();
const time = query.get("period");
useEffect(() => {
if (time === "yearly") {
const yearlyData = duration["yearly"];
setSpanData(yearlyData);
} else if (time === "weekly") {
const weeklyData = duration["weekly"];
setSpanData(weeklyData);
} else if (time === "quarterly") {
const quarterlyData = duration["quarterly"];
setSpanData(quarterlyData);
} else if (time === "monthly") {
const monthlyData = duration["monthly"];
setSpanData(monthlyData);
} else if (time === "6 months") {
const halfYearlyData = duration["half-yearly"];
setSpanData(halfYearlyData);
}
}, [time, duration]);
/**
*
* #param {*} event
* #param {*} newValue
* on tab change
*/
// eslint-disable-next-line
const handleTabChange = (event, newValue) => {
setTabValue(newValue);
};
useEffect(() => {
if (duration !== "") {
getOverviewData(
timeSpan,
duration.monthly[0].period.to,
duration.monthly[0].period.from,
dataType,
_reference
);
}
}, [duration]);
/**
* Year change
* #param {*} event
*/
const handleYearChange = (event) => {
const v = event.target.value;
setYear(v);
setTimeSpan(query.get("period"));
getDuration(v);
};
/**
* Span change
* #param {*} event
*/
const handleSpanChange = (event) => {
const value = event.target.value;
const reValue = value === "6 months" ? "half-yearly" : value;
setTimeSpan(value);
getOverviewData(
value,
duration[reValue][0].period.to,
duration[reValue][0].period.from,
dataType,
_reference
);
setDataTo(duration[reValue][0].period.to);
setDataFrom(duration[reValue][0].period.from);
setIncidencesSection(false);
setOverViewSelect("");
};
const handleSpanTabChange = (data, i) => {
setSelectTab(i);
setDataTo(data.period.to);
setDataFrom(data.period.from);
getOverviewData(
time,
data.period.to,
data.period.from,
dataType,
_reference
);
setDurationLabel(data.label);
setOverViewSelect("");
setIncidencesSection(false);
};
const handleDataTypeChange = (event) => {
setDataType(event.target.value);
getOverviewData(time, dataTo, dataFrom, event.target.value, _reference);
setIncidencesSection(false);
setOverViewSelect("");
};
const handleOverViewClick = (data) => {
clearAnalysis();
setOverViewSelect(data);
const value = time === "6 months" ? "half_yearly" : time;
getAnalysis(data, value, dataFrom, dataTo, dataType, 1, _reference);
setIncidencesSection(true);
};
const handlePageNext = (pageNo) => {
getAnalysis(
overViewSelect,
time,
dataFrom,
dataTo,
dataType,
pageNo,
_reference
);
};
// useEffect(() => {
// const localValue = JSON.parse(
// window.localStorage.getItem("incidenceState")
// );
// console.log("localValue", localValue);
// }, []);
return (
<div className={classes.dispenseRoot}>
<Paper
elementType="div"
elevation={5}
square={true}
variant="elevation"
className={classes.topContainer}
>
<div className={classes.topContainerDiv}>
<div className={classes.topContainerLeft}>
<div className={classes.headerTextDiv}>
<p>Period</p>
</div>
<FormControl variant="outlined" className={classes.formControl}>
<Select
className={classes.containerSelect}
id="demo-simple-select-outlined"
value={timeSpan}
onChange={handleSpanChange}
>
{Object.values(period).map((span, i) => {
return (
<MenuItem key={i} value={span.toLowerCase()}>
{span.toUpperCase()}
</MenuItem>
);
})}
</Select>
</FormControl>
<FormControl variant="outlined" className={classes.formControl}>
<Select
className={classes.containerSelect}
id="demo-simple-select-outlined"
value={year}
onChange={handleYearChange}
>
<MenuItem value={2020}>2020</MenuItem>
<MenuItem value={2019}>2019</MenuItem>
<MenuItem value={2018}>2018</MenuItem>
<MenuItem value={2017}>2017</MenuItem>
</Select>
</FormControl>
<div className={classes.typeHeading}>
<p>Type</p>
</div>
<FormControl variant="outlined" className={classes.formControl}>
<Select
className={classes.containerSelect}
id="demo-simple-select-outlined"
value={dataType}
onChange={handleDataTypeChange}
>
{Object.keys(type).map((key) => {
return (
<MenuItem key={key} value={key}>
{key.toUpperCase()}
</MenuItem>
);
})}
</Select>
</FormControl>
</div>
</div>
</Paper>
<div className={classes.dispensingIncidentsTabDiv}>
<Paper square>
<Tabs
value={tabValue}
indicatorColor="primary"
textColor="primary"
onChange={handleTabChange}
variant="scrollable"
scrollButtons="on"
aria-label="scrollable auto tabs example"
>
<Tab
className={classes.dispensingTab}
label="INCIDENCES"
{...a11yProps(0)}
/>
</Tabs>
</Paper>
</div>
{incidencesSection ? (
<div className={classes.incidencesAccordionDiv}>
<IncidenceAccordion
handlePageNext={handlePageNext}
incidenceAnalysisDataArray={incidenceAnalysisDataArray}
data={incidenceAnalysisData}
title={"Incidences"}
/>
</div>
) : null}
</div>
);
};
Every time when you call setX() useState() hook, try to save the value in the localStorage. What I'd do in short is:
const [Value, setValue] = useState("");
const handleSetValue = (newValue) => {
setValue(newValue);
window.localStorage.setItem("Value", newValue);
}
And I use the handleSetValue() instead of setValue. And the same way, in the useEffect() hook, I'll try to load all the values this way:
useEffect(() => {
// While loading, load the state from the localStorage.
if (window.localStorage.getItem("Value"))
setValue(window.localStorage.getItem("Value"));
}, [])
The above code will help you to persist your data from reloads.
Related
I have a specific component that renders twice. Basically, I am generating two random numbers and displaying them on the screen and for a split second, it shows undefined for both then shows the actual numbers. To test it further I did a simple console.log and it indeed logs it twice. So my question splits into two - 1. Why does the typography shows undefined for a split second before rendering? 2 - Why does it render twice?
Here's the related code:
Beginnging.js - this a counter that counts down from 3 and fires an RTK action, setting gameStart true.
function Beginning() {
const [count, setCount] = useState(3);
const [message, setMessage] = useState("");
const dispatch = useDispatch();
const countRef = useRef(count);
useEffect(() => {
countRef.current = count;
}, [count]);
const handleCount = () => {
if (countRef.current === 1) {
return setMessage("GO");
}
return setCount((count) => count - 1);
};
useEffect(() => {
const interval = setInterval(() => {
handleCount();
}, 1000);
if (message==='GO') {
setTimeout(() => {
dispatch(start());
}, 1000);
}
return () => clearInterval(interval);
}, [count, message]);
return (
<>
<Typography variant="h1" fontStyle={'Poppins'} fontSize={36}>GET READY...</Typography>
<Typography fontSize={48} >{count}</Typography>
<Typography fontSize={60} >{message}</Typography>
</>
);
}
export default Beginning;
AdditionMain.js - based on gameStart, this is where I render Beginning.js, once it counts down, MainInput is rendered.
const AdditionMain = () => {
const gameStart = useSelector((state) => state.game.isStarted);
const operation = "+";
const calculation = generateNumbersAndResults().addition;
if (!gameStart){
return <Beginning/>
}
return (
<>
<MainInput operation={operation} calculation={calculation} />
</>
);
};
export default AdditionMain;
MainInput.js - the component in question.
const MainInput = ({ operation, calculation }) => {
const [enteredValue, setEnteredValue] = useState("");
const [correctValue, setCorrectValue] = useState(false);
const [calculatedNums, setCalculatedNums] = useState({});
const [isIncorrect, setIsIncorrect] = useState(false);
const [generateNewNumbers, setGenerateNewNumbers] = useState(false);
const [haveToEnterAnswer, setHaveToEnterAnswer] = useState(false);
const dispatch = useDispatch();
const seconds = useSelector((state) => state.game.seconds);
const points = useSelector((state) => state.game.points);
const lives = useSelector((state) => state.game.lives);
const timerValid = seconds > 0;
const newChallenge = () => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
dispatch(restart());
};
const handleCount = () => {
dispatch(loseTime());
};
useEffect(() => {
let interval;
if (timerValid) {
interval = setInterval(() => {
handleCount();
}, 1000);
}
return () => {
console.log("first");
clearInterval(interval);
};
}, [timerValid]);
useEffect(() => {
setCalculatedNums(calculation());
setGenerateNewNumbers(false);
setCorrectValue(false);
setEnteredValue("");
}, [generateNewNumbers]);
const submitHandler = () => {
if (correctValue) {
setGenerateNewNumbers(true);
dispatch(gainPoints());
dispatch(gainTime());
}
if (+enteredValue === calculatedNums.result) {
setCorrectValue(true);
} else if (enteredValue.length === 0) {
setHaveToEnterAnswer(true);
} else {
setIsIncorrect(true);
dispatch(loseLife());
}
};
const inputValueHandler = (value) => {
setIsIncorrect(false);
setHaveToEnterAnswer(false);
setEnteredValue(value);
};
const submitOrTryNewOne = () => {
return correctValue ? "Try new one" : "Submit";
};
return (
<>
<Navbar />
{console.log("hello")}
{seconds && lives > 0 ? (
<>
<GameInfo></GameInfo>
<Container component="main" maxWidth="xs">
<Box
sx={{
marginTop: 8,
display: "flex",
flexDirection: "column",
alignItems: "center",
}}
>
<Typography>
Fill in the box to make the equation true.
</Typography>
<Typography fontSize={28}>
{operation !== "/"
? `${calculatedNums.number1} ${operation} ${calculatedNums.number2}`
: `${calculatedNums.number2} ${operation} ${calculatedNums.number1}`}{" "}
=
</Typography>
<TextField
inputProps={{ inputMode: "numeric", pattern: "[0-9]*" }}
type="number"
name="sum"
id="outlined-basic"
label=""
variant="outlined"
onChange={(event) => {
inputValueHandler(event.target.value);
}}
disabled={correctValue}
value={enteredValue}
></TextField>
{haveToEnterAnswer && enterAnswer}
{correctValue && correctAnswer}
{isIncorrect && wrongAnswer}
<Button
type="button"
sx={{ marginTop: 1 }}
onClick={() => submitHandler()}
variant="outlined"
>
{isIncorrect ? "Try again!" : submitOrTryNewOne()}
</Button>
</Box>
</Container>
</>
) : (
<>
<Typography>GAME OVER</Typography>
<Typography>Final Score: {points}</Typography>
<Button onClick={newChallenge}>New Challenge</Button>
</>
)}
</>
);
};
export default MainInput;
PS: I'm trying to figure out how to get this running on Codesandbox
The problem is that you render the page before the calculaed nums are established.
Try this instead. It will prevent the element from displaying until nums are generated.
const [calculatedNums, setCalculatedNums] = useState(null);
// This is down in your render area. Only render once calculatedNums are non-null.
{ calculatedNums &&
<Typography fontSize={28}>
operation !== "/"
? `${calculatedNums.number1} ${operation} ${calculatedNums.number2}`
: `${calculatedNums.number2} ${operation} ${calculatedNums.number1}`}{" "}
=
</Typography>
}
In addition, you are probably generating your numbers twice, because you will generate new numbers on initial load, but then when you hit setGenerateNewNumbers(true); it will trigger a new calculation, which will then set the generatedNewNumbers to false, which will call the calc again, since it triggers whenever that state changes. It stops after that because it tries to set itself to false again and doesn't change.
You are changing a dependency value within the hook itself, causing it to run again. Without looking a lot more into your program flow, a really hacky way of fixing that would just be to wrap your useEffect operation inside an if check:
useEffect(()=>{
if (generateNewNumbers === true) {
//All your stuff here.
}
}, [generateNewNumbers]
That way, it won't run again when you set it to false.
I have a table where I'm setting data inside a useEffect from an api. My filter logic iterates through the "rows" variable which is being set inside this useEffect. However, every-time the user searches via an input which has an onChange event the useEffect setRows I believe is setting the data over and over again.
What would be a better way to set the data so it doesn't conflict with my filtering logic?
//State
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
//useEffect to setData
useEffect(() => {
//setDocuments from claimStore when component mounts
setDocuments(claimsStore.getIncomingDocuments());
//setRows from documents when component mounts
setRows(
documents.map((document) =>
createData(
document.documentAuthor ?? '',
document.documentMetadataId.toLocaleString(),
document.documentMetadataId.toLocaleString(),
document.documentName ?? '',
document.documentSource ?? '',
document.documentType,
document.featureId ?? '',
document.mimeType,
document.uploadDateTime,
),
),
);
}, [claimsStore, documents]);
//Filter logic that updates rows as user input values captured
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
//Filter documents based on user input
const updatedList = rows.filter((document) => {
return (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
);
});
//Trigger render with updated values
setRows(updatedList);
};
Use of filterBySearch:
<TableCell align={'center'} className={classes.tableCell}>
<input
value={filters.featureId}
onChange={(e) => filterBySearch(e)}
name="featureId"
className={classes.inputCell}
/>
</TableCell>
This is one of the things useMemo is good for: Have an array of filtered rows, that you update as necessary when rows or filters changes:
const [documents, setDocuments] = useState<IDocument[]>([]);
const [rows, setRows] = useState<Data[]>([]);
// ...
const filteredRows = useMemo(
() => rows.filter((document) => (
document.documentAuthor.toLowerCase().includes(filters.documentAuthor.toLowerCase()) &&
document.documentName.toLowerCase().includes(filters.documentName.toLowerCase()) &&
document.documentSource.toLowerCase().includes(filters.documentSource.toLowerCase()) &&
document.documentType.includes(filters.documentType === 'All' ? '' : filters.documentType) &&
document.featureId.includes(filters.featureId)
)),
[rows, filters]
);
Then display filteredRows, not rows.
With that change, filterBySearch just sets the filter, it doesn't actually do the filtering:
const filterBySearch = (e: React.ChangeEvent<HTMLInputElement>) => {
const { name, value } = e.target;
const newFilters = { ...filters, [name]: value };
//Update filters with user input
setFilters(newFilters);
};
useMemo will only call your callback when either rows or filters changes; otherwise, it'll just return the previous filtered array.
Here's a simplified demo — it shows words filtered by whatever you type in the filter, and randomly adds a word once every couple of seconds (this demonstrates that the filtering is repeated when the filter changes or when the rows change):
const { useState, useEffect, useRef, useMemo } = React;
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
return (
<div>
<div>
Filter: <input type="text" value={filter} onChange={filterChange} />
</div>
Rows - showing {filteredRows.length} of {rows.length} total:
<div>
{filteredRows.map((row) => (
<div key={row.id}>{row.value}</div>
))}
</div>
</div>
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(<Example />);
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
React's documentation says that useMemo is just for performance enhancement, it isn't a semantic guarantee (basically, React may call your callback even when nothing has actually changed). If you want a semantic guarantee, you can do it with a ref. You can even wrap that up into a hook that provides the semantic guarantee — I call it useHardMemo:
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
Live Example:
const { useState, useEffect, useRef, createElement } = React;
const useHardMemo = (fn, deps) => {
const ref = useRef(null);
let { current } = ref;
if (current) {
// Consistency check
if (
(deps && !current.deps) ||
(!deps && current.deps) ||
(deps && deps.length !== current.deps.length)
) {
throw new Error(
`Invalid call to useHardMemo, the dependency array must either always be present ` +
`or always be absent, and if present must always have the same number of items.`
);
}
}
if (!current || !deps?.every((dep, index) => Object.is(current.deps?.[index], dep))) {
ref.current = current = {
deps: deps?.slice(),
value: fn(),
};
}
return current.value;
};
const words = "one two three four five six seven eight nine ten".split(" ");
let nextRowId = 1;
const Example = () => {
const [rows, setRows] = useState(
words.slice(0, 5).map((value) => ({ id: nextRowId++, value }))
);
const [filter, setFilter] = useState("");
const filteredRows = useHardMemo(() => {
console.log(`Filtering rows`);
if (!filter) {
return rows;
}
return rows.filter((row) => row.value.includes(filter));
}, [rows, filter]);
useEffect(() => {
let handle;
tick();
function tick() {
handle = setTimeout(() => {
const value = words[Math.floor(Math.random() * words.length)];
console.log(`Adding "${value}"`);
setRows((rows) => [...rows, { id: nextRowId++, value }]);
tick();
}, 2000);
}
return () => {
clearTimeout(handle);
};
}, []);
const filterChange = ({ currentTarget: { value } }) => {
console.log(`Setting filter to "${value}"`);
setFilter(value);
};
// I'm using `createElement` because I had to turn off SO's hopelessly outdated Babel because
// I wanted to be able to use optional chaining and such; so I couldn't use JSX.
// return (
// <div>
// <div>
// Filter: <input type="text" value={filter} onChange={filterChange} />
// </div>
// Rows - showing {filteredRows.length} of {rows.length} total:
// <div>
// {filteredRows.map((row) => (
// <div key={row.id}>{row.value}</div>
// ))}
// </div>
// </div>
// );
return createElement(
"div",
null,
createElement(
"div",
null,
"Filter: ",
createElement("input", { type: "text", value: filter, onChange: filterChange })
),
`Rows - showing ${filteredRows.length} of ${rows.length} total:`,
createElement(
"div",
null,
filteredRows.map((row) => createElement("div", { key: row.id }, row.value))
)
);
};
const root = ReactDOM.createRoot(document.getElementById("root"));
root.render(createElement(Example));
<div id="root"></div>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react/18.1.0/umd/react.development.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/react-dom/18.1.0/umd/react-dom.development.js"></script>
Bit of a noob to redux but this appears to be quite a difficult question! I hope someone may be able to help me :)
I have build a page where you can input a search for different types of jobs. From this, it will make a get request to my DB and get all the info on this job. As this page is multi-levelled, I want to use redux to dispatch and pass the state throughout. This will help me pass my data on the job, e.g Data Analyst, through to each component so it can use the data and populate fields.
However, this was how my input field was originally setup:
export function SearchBarComp(props) {
const [isExpanded, setExpanded] = useState(false);
const [parentRef, isClickedOutside ] = useClickOutside();
const inputRef = useRef();
const [searchQuery, setSearchQuery] = useState("");
const [isLoading, setLoading] = useState(false);
const [jobPostings, setjobPostings] = useState([]);
const [noRoles, setNoRoles] = useState(false)
const isEmpty = !jobPostings || jobPostings.length === 0;
const changeHandler = (e) => {
//prevents defaulting, autocomplete
e.preventDefault();
if(e.target.value.trim() === '') setNoRoles(false);
setSearchQuery(e.target.value);
}
const expandedContainer = () => {
setExpanded(true);
}
//LINK THE BACKEND!
const prepareSearchQuery = (query) => {
//const url = `http://localhost:5000/api/role/title?title=${query}`;
const url = `http://localhost:5000/api/role/titlerole?title=${query}`;
//replaces bad query in the url
return encodeURI(url);
}
const searchRolePosition = async () => {
if(!searchQuery || searchQuery.trim() === "")
return;
setLoading(true);
setNoRoles(false);
const URL = prepareSearchQuery(searchQuery);
const response = await axios.get(URL).catch((err) => {
console.log(err);
});
if(response) {
console.log("Response", response.data);
if(response.data && response.data === 0)
setNoRoles(true);
setjobPostings(response.data);
}
setLoading(false);
}
useDebounce(searchQuery, 500, searchRolePosition)
const collapseContainer = () => {
setExpanded(false);
setSearchQuery("");
setLoading(false);
setNoRoles(false);
if (inputRef.current) inputRef.current.value = "";
};
// console.log("Value", searchQuery)
useEffect(()=> {
if(isClickedOutside)
collapseContainer();
}, [isClickedOutside])
return (
<SearchBarContainer animate = {isExpanded ? "expanded" : "collapsed"}
variants={containerVariants} transition={containerTransition} ref={parentRef}>
<SearchInputContainer>
<SearchIconSpan>
<SearchIcon/>
</SearchIconSpan>
<SearchInput placeholder = "Search for Roles"
onFocus={expandedContainer}
ref={inputRef}
value={searchQuery}
onChange={changeHandler}
/>
<AnimatePresence>
{isExpanded && (<CloseIconSpan key="close-icon"
inital={{opacity:0, rotate: 0}}
animate={{opacity:1, rotate: 180}}
exit={{opacity:0, rotate: 0}}
transition={{duration: 0.2}}
onClick={collapseContainer}>
<CloseIcon/>
</CloseIconSpan>
)}
</AnimatePresence>
</SearchInputContainer>
{isExpanded && <LineSeperator/>}
{isExpanded && <SearchContent>
{!isLoading && isEmpty && !noRoles && (
<Typography color="gray" display="flex" flex="0.2" alignSelf="center" justifySelf="center">
Start typing to search
</Typography>
)}
{!isLoading && !isEmpty && <>
{jobPostings.map((searchRolePosition) => (
<JobSection
title={searchRolePosition.title}
//will need to do something like ----
//people = {searchRolePosition.title && searchRolePosition.title.average}
// future implementations
/>
))}
</>}
</SearchContent>}
</SearchBarContainer>
)
}
As you can see, the main thing is the 'query' this creates a backend request to my titlerole, such as getting the data on Data Analyst. This all works in my frontend right now, but I can't pass that data down to the next component etc
So i'm looking to use redux.
I've created the following slice:
import { createSlice } from "#reduxjs/toolkit";
const jobSearchSlice = createSlice({
name: "jobsearch",
initialState: {
currentRole: null,
isFetching: false,
error: false,
},
reducers: {
jobsearchStart: (state) => {
state.isFetching = true;
},
jobsearchSuccess: (state, action) => {
state.isFetching = false;
state.currentRole = action.payload;
},
jobsearchFailure: (state) => {
state.isFetching = false;
state.error = true;
},
},
});
export const { jobsearchStart, jobsearchSuccess, jobsearchFailure } = jobSearchSlice.actions;
export default jobSearchSlice.reducer;
With this, I'm also using the following apiCalls.js file:
import { jobsearchStart, jobsearchSuccess, jobsearchFailure } from "./jobsearchSlice";
import { publicRequest } from "../requestMethods";
export const roleSearchQuery = async (dispatch, jobsearch) => {
dispatch(jobsearchStart());
try{
const res = await publicRequest.get("`http://localhost:5000/api/role/titlerole?title=${query}`", jobsearch);
dispatch(jobsearchSuccess(res.data))
}catch(err){
dispatch(jobsearchFailure());
}
};
My question is as a Redux noob, how do I implement this query functionality into a redux API request? What's the way to do this properly as I begin to tranisition this to an app which uses standard state management!
I have created a react app which will fetch the api using the values from the url params. which are modified using navigate prop without page refresh.
Here is the code.
const App = () => {
const [itemData, setItemData] = useState({});
const [itemError, setItemError] = useState({});
const [additionalData, setAdditionalData] = useState({});
const [additionalError, setAdditionalError] = useState({});
const [isLoading, setIsLoading] = useState(false);
const [showTrailer, setShowTrailer] = useState(false);
const [trailer, setTrailer] = useState({});
const [trailerError, setTrailerError] = useState({});
const [group, setGroup] = useState([])
const backend_url = process.env.REACT_APP_BACKEND;
const handleCloseTrailer = () => setShowTrailer(false);
const handleShowTrailer = () => setShowTrailer(true);
const location = useLocation();
const id = location.pathname.split("/")[2];
const [searchParams, setSearchParams] = useSearchParams();
const [people, setPeople] = useState([]);
const [groupId, setGroupId] = useState(searchParams.get("group_id"));
const navigate = useNavigate();
function handleChange(value) {
navigate(`?group_id=${value}`);
}
useEffect(() => {
const fetchMainApi = () => {
setIsLoading(true)
axios.get(`${backend_url}/api/v1/metadata?id=${id}`)
.then(function(response) {
if(response.data.content.apiId !== 'undefined') {
axios.get("API_URL")
.then(function (response) {
setAdditionalData(response.data);
})
.catch(function (error) {
setAdditionalError(error);
})
}
if(itemData && (itemData.apiId !== 'null' || 'undefined')) {
axios.get("API_URL")
.then(function(response) {
setTrailer(response.data)
})
.catch(function(error) {
setTrailerError(error)
})
}
if(type === "cat" && itemData.children) {
setGroup(itemData.children)
}
if(type === "cat" && itemData.children)
axios.get("API_URL" + groupId)
.then(function (response) {
setPeople(response.data.content.children);
})
.catch(function (error) {
console.log(error);
});
setItemData(response.data.content)
})
.catch(function(error) {
setItemError(error)
})
setIsLoading(false)
}
fetchMainApi()
}, [backend_url,id,type,itemData.apiId,itemData.api])
return (
<>
<Form.Select onChange={event => handleChange(event.target.value)} aria-label="Default select example">
<option>Group All</option>
{cluster.map((person, index) => (
<option key={guid()} value={group.id}>{group.name}</option>
))}
</Form.Select>
<People people={people}/>
</>
);
};
export default App;
Here is the People component
const People = ({people}) => {
return (
<Row className="m-2 pt-2">
<h2 className="color-white">People</h2>
{people && people.length > 0 && (people.map((people, index) => (
<Col key={index} className="p-lg-4 p-sm-3" xs={12} sm={6} md={4} lg={3} xl={3}>
....
</Col>
)))}
{ (!people || people.length === 0) && (<h5 className="color-white">No Persons Found</h5>) }
</Row>
);
};
export default People;
Working
The select menu updates the query param and then the value of param is taken inside useEffect hook when then provides the data.
Every thing works well but the problem is to update the data inside the component i need to refresh the page when then works as expected.
Is there a way to change or update only the people component without a page refresh.
I have a custom component of
const Search = (props) => {
const { type: TYPE, name: NAME, label: LABEL, onSelect, filter, value } = props;
const [data, setData] = useState([]);
const [select, setSelect] = useState(value || -1);
const applyFilter = (data) => {
let result = data;
if (filter) {
result = filter(data);
}
return result;
};
useEffect(() => {
getLookupData(TYPE)
.then((response) => {
setData(response);
})
.catch((error) => {
if (error === HttpStatus.NOT_FOUND) {
setData([]);
} else {
throw error;
}
});
}, [TYPE]);
useEffect(() => {
if (value) {
setSelect(value);
}
}, [value]);
const options = applyFilter(data).map((item) => (
<MenuItem value={item.id} key={item.id}>
{item[NAME]}
</MenuItem>
));
const handleChange = (event) => {
setSelect(event.target.value);
onSelect && onSelect(event);
};
const { classes } = props;
const labelProps = { ...(props.required && { required: props.required }) };
return (
<FormControl className={classes.formControl} id={NAME} error={props.error}>
<FormControlLabel control={<InputLabel htmlFor={NAME} {...labelProps}>{LABEL}</InputLabel>} />
<Select
name={TYPE}
value={select}
onChange={handleChange}
disabled={props.disabled || options.length === 0}
input={<Input name={TYPE} id={NAME} />}
>
<MenuItem value=''>
<em>None</em>
</MenuItem>
{options}
</Select>
</FormControl>
);
};
It takes in a menu item by default of None, however i want to pass this None menuitem as a prop and call it when i want too, as for some fields i want the option of 'none' and for some fields i do not want the option of none, currently the system is hard coded to have none appear on every drop down and i dont wish for this to happen
<Search
required
type="contractOptionToExtend"
name="optionToExtend"
label="Option To Extend"
id="contractOptionToExtend"
onSelect={change.bind(null, 'contractOptionToExtend')}
error={touched.contractOptionToExtend && Boolean(errors.contractOptionToExtend)}
value={values.contractOptionToExtend}
/>
On the above field i want to say wether or not i want the None option, I can solve this by passing it as a prop to the Search component but im not totally sure how, I have done something similar for props.required as you can see but currently i cannot do the same for the menu item.
{props.showNone
? <MenuItem value=''>
<em>None</em>
</MenuItem>
: null
}
then within the desired field
showNone={true}
credit to #Abhey Sehgal