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!
Related
This is not happening in any other slice I created, and it's driving me nuts. I'm out of ideas to try. What I want is simple, pull the payload from my Slice so I can set it to my state and use it! But somehow I am not capturing it. I put a console.log on the Slice for the payload and I am successfully fetching See Pic Below
Now checkout this screenshot with the component on the screen. I noticed that I was getting an error for the 'country' being undefined in pullrank in line 285 which it doesnt mean the path in incorrect or that country doesnt exist, it just breaks because the whole data is empty. If I refresh it all comes up normally, so I commented out the part where I am setting setMinRange and setMaxRange and that's when I realize that allmapdata was setting up empty, Twice nontheless! And then once again with the data in it. Obviously a racing condition, the API is fetching before the data is called, so how do I delay this API call?
Here is my mapSlice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import axios from "axios";
const KEY = process.env.REACT_APP_API_KEY
const BASE_URL = process.env.REACT_APP_BASE_URL
const GLOBALVIEWS_API = `${BASE_URL}/countrymap`
const initialState = {
mapdata:[],
status: 'idle', //'idle', 'loading', 'succeeded', 'failed'
error:null
}
export const fetchMapData = createAsyncThunk(
'mapdata/fetchMapData',
async (id) => {
try {
console.log("Firing Map reducer");
const response = await axios.get(
GLOBALVIEWS_API,
{
headers: {
'Content-Type': 'application/json',
'X-API-KEY': KEY,
},
params: {
titleId: id,
}
}
)
return response.data;
} catch (error) {
console.error('API call error:', error.message);
}
}
)
const mapSlice = createSlice({
name: 'mapdata',
initialState,
reducers:{
fetchMap(state, action) {},
},
extraReducers(builder) {
builder
.addCase(fetchMapData.pending, (state, action) => {
state.status = 'loading'
})
.addCase(fetchMapData.fulfilled, (state, action) => {
state.status = 'succeeded'
const loadedMapData = action.payload.Item.season_met
state.mapdata = loadedMapData
console.log("loadedMapData: ", loadedMapData);
})
.addCase(fetchMapData.rejected, (state, action) => {
state.status = 'failed'
state.error = action.error.message
})
}
})
// SELECTORS
// allMapData and fetchMap aren't both needed, I'm just trying different ways
export const allMapData = (state) => state.mapdata.mapdata;
export const {fetchMap} = mapSlice.actions;
export const getMapStatus = (state) => state.mapdata.status;
export const getMapError = (state) => state.mapdata.error;
export default mapSlice.reducer
Here's my component (reduced for brevity)
const MetricsCharts = ({selectedIndex}) => {
//STATES
const [minrange, setMinRange] = useState();
const [maxrange, setMaxRange] = useState();
const [map, setMap] = useState([])
const colorScale = scaleLog().domain([minrange, maxrange]).range(["#BAC7FF", "#6128BD"]);
const {id} = useParams();
const dispatch = useDispatch();
const allmapdata = useSelector(allMapData)
const mapdata = useSelector(fetchMap)
const mapStatus = useSelector(getMapStatus)
const error = useSelector(getMapError)
useEffect(() => {
if (mapStatus === 'idle') {
dispatch(fetchMapData(id))
}
setMap(allmapdata)
// const pullrank = allmapdata[selectedIndex].country.map(data => data.lifetime_viewing_subs)
// setMinRange(Math.min(...pullrank))
// setMaxRange(Math.max(...pullrank))
console.log("allmapdata: ", allmapdata);
}, [id, selectedIndex, dispatch, allmapdata, mapStatus, mapdata])
let map_component;
if (mapStatus === 'loading') {
map_component =
<Box className="loading">
<LoadingButton loading
loadingIndicator={
<CircularProgress color="primary" size={50} />
}>
</LoadingButton>
<Typography variant='subtitle2'>Loading content, please wait...</Typography>
</Box>
} else if (mapStatus === 'succeeded') {
map_component =
<>
{map.length > 0 && //checking if something is in the state
<Box sx={{mt:2}}>
<ReactTooltip>{content}</ReactTooltip>
<ComposableMap
width={900}
height={400}
data-tip=""
projectionConfig={{ rotate: [-10, 0, 0], scale:147 }}>
<Sphere stroke="#000" strokeWidth={0.3} />
<Graticule stroke="#000" strokeWidth={0.3} />
<Geographies geography={geoUrl}>
{({ geographies }) => geographies.map((geo, index) => {
const countries = map[selectedIndex].country.find( (s) => s.ISO3 === geo.properties.ISO_A3);
return (
<Fragment key={index}>
<Geography
key={geo.rsmKey}
geography={geo}
onMouseEnter={() => {
setContent(
countries
? `${geo.properties.NAME_LONG} — ${rounded(Math.round(countries.lifetime_viewing_subs))}`
: ""
);
}}
fill={
countries
? colorScale(countries["lifetime_viewing_subs"])
: "#333"
}
/>
</Fragment>
);
})
}
</Geographies>
</ComposableMap>
</Box>
}
</>
} else if (mapStatus === 'failed') {
map_component =
<Box className="loading">
<Typography variant="subtitle2">{error}</Typography>
</Box>
}
return (
<div>
{map_component}
</div>
)
}
export default MetricsCharts
As you can see, I can't just give you a CODEPEN cause the map dependency is incredibly large and complex. I'm sorry about that. I do appreciate any help
I'm working on a project and wanted to try and implement an infinitely scrollable page to list users. Filtering and such works fine and all but every time the scrolling component reaches the ref element the component makes an API call to append to the list and then the parent component re-renders self completely.
const UsersList = () => {
const [searchString, setSearchString] = useState('')
const [next, setNext] = useState('')
const { userList, error, nextPage, loading, hasMore } = useFetch(next)
const [usersList, setUsersList] = useState([])
const observer = useRef()
const lastElemRef = useCallback(
(node) => {
if (loading) return
if (observer.current) observer.current.disconnect()
observer.current = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting && hasMore) {
setNext((prev) => (prev = nextPage))
setUsersList((prev) => new Set([...prev, ...userList]))
}
})
if (node) {
observer.current.observe(node)
}
},
[loading, nextPage, hasMore],
)
useEffect(() => {
setUsersList((prev) => new Set([...prev, ...userList]))
console.log(error)
}, [])
return (
<>
{loading ? (
<CSpinner variant="grow"></CSpinner>
) : (
<CContainer
className="w-100 justify-content-center"
style={{ maxWidth: 'inherit', overflowY: 'auto', height: 600 }}
>
<CFormInput
className="mt-2 "
id="userSearchInput"
value={searchString}
onChange={(e) => {
setSearchString(e.target.value)
}}
/>
{loading ? (
<CSpinner variant="grow"></CSpinner>
) : (
<>
{Array.from(usersList)
.filter((f) => f.username.includes(searchString) || searchString === '')
.map((user) => {
if (Array.from(usersList)[usersList.size - 1] === user) {
return (
<UserCard
key={user.id}
user={user}
parentRef={searchString ? null : lastElemRef}
/>
)
} else {
return <UserCard key={user.id} user={user} />
}
})}
</>
)}
</CContainer>
)}
</>
)
}
export default UsersList
This is the component entirely.
Here's my useFetch hook;
export const useFetch = (next) => {
const [userList, setUserList] = useState([])
const [loading, setLoading] = useState(true)
const [error, setError] = useState('')
const [nextPage, setNextPage] = useState(next)
const [hasMore, setHasMore] = useState(true)
useEffect(() => {
setLoading(true)
setError('')
axios
.get(next !== '' ? `${next}` : 'http://localhost:8000/api/getUsers/', {
headers: {
Authorization: 'Bearer ' + localStorage.getItem('access_token'),
},
})
.then((res) => {
setUserList((userList) => new Set([...userList, ...res.data.results]))
setNextPage((prev) => (prev = res.data.next))
if (res.data.next === null) setHasMore(false)
setLoading(false)
})
.catch((err) => {
setError(err)
})
}, [next])
return { userList, error, nextPage, loading, hasMore }
}
export default useFetch
I'm using Limit Offset Pagination provided by Django Rest Framework, next object just points to the next set of objects to fetch parameters include ?limit and ?offset added at the end of base API url. What is it that I'm doing wrong here ? I've tried many different solutions and nothing seems to work.
Solved
Apparently it was just my back-end not cooperating with my front-end so I've changed up the pagination type and now it seems to behave it self.
I have created an input field with a search term which creates a request to a backend API. To summarise, two issues:
It fetches data from my API, but it fetches ALL roles, not just ones filtered by the term.
It does not commit to the redux store.
Please see my app, it contains simply:
This is my frontend component, which is making an action dispatch based on a search term.
export function SearchBarTrialRedux(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 expandedContainer = () => {
setExpanded(true);
}
const collapseContainer = () => {
setExpanded(false);
setSearchQuery("");
setLoading(false);
setNoRoles(false);
if (inputRef.current) inputRef.current.value = "";
};
useEffect(()=> {
if(isClickedOutside)
collapseContainer();
}, [isClickedOutside])
const [term, setTerm] = useState("")
const dispatch = useDispatch();
const changeHandler = (e) => {
e.preventDefault();
fetchAsyncRoles(dispatch, {term});
}
return (
<SearchBarContainer animate = {isExpanded ? "expanded" : "collapsed"}
variants={containerVariants} transition={containerTransition} ref={parentRef}>
<SearchInputContainer>
<SearchIconSpan>
<SearchIcon/>
</SearchIconSpan>
<form onSubmit={changeHandler}>
<SearchInput placeholder = "Search for Roles"
onFocus={expandedContainer}
ref={inputRef}
value={term}
onChange={(e)=> setTerm(e.target.value)}
/>
</form>
</SearchBarContainer>
And my jobsearchSlice
import { createSlice, createAsyncThunk } from "#reduxjs/toolkit";
import { publicRequest } from "../requestMethods";
export const fetchAsyncRoles = async (dispatch, term) => {
dispatch(searchStart());
try {
const res = await publicRequest.get(`http://localhost:5000/api/role/titlerole?title=${term}`);
dispatch(searchSuccess(res.data));
console.log(res.data)
} catch (err) {
dispatch(searchFailure());
}
};
const jobsearchSlice = createSlice({
name: "jobsearchSlice",
initialState: {
isFetching: false,
roles: [],
error: false,
},
reducers: {
searchStart: (state) => {
state.isFetching = true;
},
searchSuccess: (state, action) => {
state.isFetching = false;
state.roles = action.payload;
},
searchFailure: (state) => {
state.isFetching = false;
state.error = true;
},
},
});
export const { searchStart, searchSuccess, searchFailure } = jobsearchSlice.actions;
export default jobsearchSlice.reducer;
As stated, it does create and fetch this data. This does commit it to the store under the roles key, which is great! That's what I want, however it is not filtering. E.g If we look at a role specifically like Data Scientist:
https://gyazo.com/ca4c2b142771edd060a7563b4200adf8
I should be getting just 1 key, Data Scientist.
Looking at the backend of the console.log(res), I can see that it appears my term isn't properly coming through and filtering my roles :
responseURL: "http://localhost:5000/api/role/titlerole?title=[object%20Object]"
But if I log the term, it does come through exactly as input.
What's wrong, what am I doing and how should I solve this term flowing through to filter my req?
I can confirm that if I do this on postman it works...
https://gyazo.com/10f2946c1a3807370b4792c06292b557
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 am trying to render a skeleton component on loading of the data or 404 error component if data not found so far i have tried using if else statements and logical operators so far none work properly.
const [post, setPost] = useState(null);
const [postExternal, setPostExternal] = useState([]);
const fetchPost = () => {
axios.get(`${API_ONE}/posts?id=${id}`).then((response) => {
setPost(response.data);
});
axios.get(`${API_TWO}/posts?id=${id}`).then((response) => {
setPostExternal(response.data);
});
return;
};
const location = useLocation();
const id = location.pathname.split('/')[2];
useEffect(() => {
fetchPost();
}, [id]);
{
post && postExternal && (
<div>
<h1>{post.title}</h1>
<img src={postExternal} />
</div>
);
}
{
!post &&
(post && post.id === postExternal.id ? (
<NotFound message='not found' />
) : (
<SkeletonItemPage />
));
}
Note: The data is fetched from two different apis
Define a loading state variable as the follow:
const [isLoading, setIsLoading] = useState(false);
Then your fetchPosts function would be like:
const fetchPost = () => {
const apiOnePromise = axios.get(`${API_ONE}/posts?id=${id}`);
const apiTwoPromise = axios.get(`${API_TWO}/posts?id=${id}`);
//toggle loader
setLoading(true);
Promise.all([apiOnePromise, apiTwoPromise])
.then(values => {
//handle your responses here
})
.finally(() => {
//toggle loader again
setLoading(false);
})
};
Using this loading variable you can differentiate between the statues, so with your jsx you can do:
{ loading && <SkeletonItemPage /> }
{ !loading && posts.length === 0 && <NotFound message='not found' /> }