Reducer gets triggered but subscribe function not fetching the change [React-Redux] - javascript

reducer shows that I've received doc data from Firestore. docStore.subscribe is listening, but is not updating with latest doc data.
Expected outcome: upon page load, will get docId from URL, and query Firestore. Upon receiving data, update the store, and subscribe to populate the view with doc information.
homepage.js
const Homepage = ({ docId }) => {
const [doc, setdoc] = useState(false);
console.log(docId); // <-- 123
docStore.subscribe(() => {
console.log('docStore state changed:', docStore.getState()); // <-- docStore state changed: undefined
setdoc(docStore.getState());
})
return (
<div>
<div>{docId}</div> {/* 123 */}
<div>{doc.docName}</div> {/* blank */}
</div>
);
};
reducer.js
export default function reducer(state = {}, action) {
switch (action.type) {
case docTypes.LOAD_DOC_PAGE:
firebase.firestore().collection("docs")
.where('docId', '==', action.payload.docId.toLowerCase())
.get()
.then(function (data) {
if (data.docs.length === 1) {
state = data.docs[0].data();
}
console.log('gotten doc', state) // <-- gotten doc data
return state;
});
}
}

Your reducer is very broken. Reducers must never make async calls!. That API call needs to be moved somewhere else entirely, and then you should dispatch an action that will cause the reducer to run and calculate an updated state.
Also, you generally shouldn't subscribe to the store yourself. Use the React-Redux connect and useSelector APIs to extract data needed by components from the store state.

Related

Passing Async State to Next.js Component via Prop

I'm fetching WordPress posts asynchronously via getStaticProps()...
export async function getStaticProps({ params, preview = false, previewData }) {
const data = await getPostsByCategory(params.slug, preview, previewData)
return {
props: {
preview,
posts: data?.posts
},
}
}
... and passing them to useState:
const [filteredArticles, setFilteredArticles] = useState(posts?.edges)
Then, I pass the state to a component:
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<ArticleGrid myArticles={filteredArticles} />
This is necessary because another component will setFilteredArticles with a filter function.
But when we are passing the state to ArticlesGrid, the data is not ready when the component loads. This is confusing to me since we passing the state within a router.isFallback condition.
Even if we set state within useEffect...
const [filteredArticles, setFilteredArticles] = useState()
useEffect(() => {
setFilteredArticles(posts)
}, [posts?.edges])
... the data arrives too late for the component.
I'm new to Next.js. I can probably hack my way through this, but I assume there's an elegant solution.
Let's look at some useEffect examples:
useEffect(() => {
console.log("Hello there");
});
This useEffect is executed after the first render and on each subsequent rerender.
useEffect(() => {
console.log("Hello there once");
}, []);
This useEffect is executed only once, after the first render.
useEffect(() => {
console.log("Hello there when id changes");
}, [props.id]);
This useEffect is executed after the first render, every time props.id changes.
Some possible solutions to your problem:
No articles case
You probably want to treat this case in your ArticleGrid component anyway, in order to prevent any potential errors.
In ArticleGrid.js:
const {myArticles} = props;
if(!myArticles) {
return (<div>Your custom no data component... </div>);
}
// normal logic
return ...
Alternatively, you could also treat this case in the parent component:
{
router.isFallback ? (
// If we're still fetching data...
<div>Loading…</div>
) : (
<>
{
filteredArticles
? <ArticleGrid myArticles={filteredArticles} />
: <div>Your custom no data component... </div>
}
</>
)
}
Use initial props
Send the initial props in case the filteres haven't been set:
const myArticles = filteredArticles || posts?.edges;
and then:
<ArticleGrid myArticles={myArticles} />

React Redux : use Selector is called before dispatch

I'm creating a react app with redux.
I need the lists of french departements for all pages in my app, so I put it in redux state.
I dispatch the action in the App component in the useEffect hook (Note I use an other useEffect in the component, but when the action is in the other block it's not working too)
I have a page where I need to use this list, so I select it with the useSelector hook.
But it returns an empty object, I have an error telling me dpts.map is not a function
I think the action is dispatching after the page has rendered, because I when I log the response of the api call in the action, it appears after the log of the useSelector result.
I'm using another state property in another page, but it seems to work with the other page.
App.jsx
const dispatch = useDispatch();
useEffect(() => {
dispatch(getDpts());
}, [dispatch])
Here is the action associated with :
dpts.actions.js
import axios from "axios";
export const GET_DPTS = "GET_DPTS";
export const getDpts = () => {
return async (dispatch) => {
try {
const res = await axios({
method: "get",
url: "https://geo.api.gouv.fr/departements",
});
console.log("done : " + res)
dispatch({ type: GET_DPTS, payload: res.data });
} catch (err) {
(err) => console.log("DPTS FETCH ERROR --- " + err);
}
};
};
Map.jsx
function DptCtl() {
// Control
const map = useMap();
// List of dpts and provinces
const dpts= useSelector(dptsSelector);
console.log(dpts);
return (
<>
<input type="text" list="dpt-ctl-list" placeholder="Filtrer par département"/>
<datalist id="dpt-ctl-list">
{dpts.map((dpt, index) =>
<option value={dpt.code} key={index}>{dpt.nom}</option>
)}
</datalist>
</>
)
}
It depends on how you are initializing your state in the reducer.
for example you create a reducer with this initial state:
const initialState={}
later, based on actions, the state changes to this:
{dpts:someDataArray}
the problem is that you have a dpts.map somewhere in your app, since dpts is undefined in the beginning you receive that error that dpts.map is not a function.
the solution to this is simply put dpts in the initialState as an empty array:
const initialState={dpts:[]}
if that is not the issue with your code, meaning dpts isn't undefined in the initialState, it is probably initialized as a string or an object which don't have map methods.

React redux reducer as UseEffect dependency causes infinite loop

I am just diving deep into React. But the useEffect React hook still got me confused. I know that I can pass dependencies as an array to it to control rendering of the component. I have used props and local state to do and it works.
What's got me still confused is when I pass redux reducer as a dependency. It causes an infinite loop of rendering the component.
users component
const usersComp = () => {
const users = useSelector(state => state.users);
useEffect(
// Fetch users and update users state
useDispatch().dispatch(getUsers)
,[users]) // <-- Causes an infinite loop!!
if(users.length){
return( users.map(user => <p>{user}</p>))
}
}
getUsers Redux Thunk function
export async function getUsers(dispatch, getState) {
fetch(endpoint)
.then(response => response.json())
.then(users => {
dispatch({type: GET_USERS, payload: users})
}).catch(err => console.error("Error: ", err));
}
users reducer
export default function usersReducer(state = [], action) {
switch (action.type) {
case GET_USERS : {
return [...state, action.payload]
}
}
}
From what I understand, users start off as an empty array, and then gets filled with data from an API call. So useEffect should fire twice; when the component has just mounted and then when users state changes from the API call. So what's causing the infinite loop?
Remove users from the useEffect dependency, because you want to fetch users when component mounts, not each time the users is changed.
useEffect(
useDispatch().dispatch(getUsers)
,[]) // Now, it will fetch users ONLY ONCE when component is mounted
Explanation:
// Case 1
useEffect(() => {
console.log("Mounted") // Printed only once when component is mounted
}, [])
// Case 2
useEffect(() => {
console.log("users changed") // Printed each time when users is changed
}, [users])
So, if you do fetch in Case 2, it will change users which will retrigger the hook which will fetch the users again which changes the users and causes the hook to retrigger → This is an infinite loop.
Update:
Why is state.users getting changed (in this code), as detected by useEffect, even when values of state.users are "SAME" (Same values)?
Whenever GET_USERS action is dispatched, reducer returns new state ({ ...state, users: action.payload }). It does so even when value of action.payload holds the same value of users.
This is why useEffect receives new users array. (They do shallow comparison.)
Note that, [1, 2,3] is not equal to [1, 2,3] i.e. [1, 2,3] === [1, 2,3] returns false.
If for some reason, you want to return the same redux state, do return state in the reducer. This is often what we do in the default case of switch of reducer.

How can manipulate redux data after dispatch it?

I have a search screen, contain Input And TopTabs "Songs, Artists",
When I get data from API after a search I make two things
1- I setState to appear the TopTab Component "true/false"
2- dispatch an action to save Songs & Artists Data in redux store.
that works fine.
But in topTab component, as I say before I have tow tabs "songs, artists"
For example, In the Songs component, I want to manipulate the data to achieve my case so in componentDidMount I Map the songs array from redux and push the new data into the component state.
But it's not working fine!
At the first time, I got songs from redux as empty [] although it's saved successfully in redux store when I get data from API
So how can I handle this case to not mutate the data?
Search.js "Main screen"
onSearch = async () => {
const {searchText} = this.state;
if (searchText.length > 0) {
this.setState({onBoarding: false}); // to appear the TopTab Component
try {
let response = await API.post('/search', {
name: searchText,
});
let {
data: {data},
} = response;
let artists = data.artists.data;
let songs = data.traks.data;
this.props.getResult(songs, artists);
}
catch (err) {
console.log(err);
}
}
render(){
<View style={styles.searchHeader}>
<Input
onChangeText={text => this.search(text)}
value={this.state.searchText}
onSubmitEditing={this.onSearch}
returnKeyType="search"
/>
</View>
{this.state.onBoarding ? (
<SearchBoard />
) : (
<SearchTabNavigator /> // TopTabs component
)}
}
SongsTab
...
componentDidMount() {
console.log('props.songs', this.props.songs); // Empty []
let All_tunes = [];
if (this.props.songs?.length > 0) {
console.log('mapping...');
this.props.songs.map(track =>
All_tunes.push({
id: track.id,
name: track.name,
url: URL + track.sounds,
img: URL + track.avatar,
}),
);
this.setState({All_tunes});
}
}
...
const mapStateToProps = state => {
return {
songs: state.searchResult.songs,
};
};
Edit
I fix the issue by using componentDidUpdate() life cycle
If you have any other ways tell me, please!
SongsTab
manipulateSongs = arr => {
let All_tunes = [];
arr.map(track =>
All_tunes.push({
id: track.id,
name: track.name,
url: URL + track.sounds,
img: URL + track.avatar,
}),
);
this.setState({All_tunes});
};
componentDidMount() {
if (this.props.songs?.length > 0) {
this.manipulateSongs(this.props.songs);
console.log('mapping...');
}
}
componentDidUpdate(prevProps) {
if (prevProps.songs !== this.props.songs) {
this.manipulateSongs(this.props.songs);
}
}
The problem you're referring to has to do with the way asynchronous code is handled in JavaScript (and in turn react-redux). When your component initially mounts, your redux store passes its initial state to your SongsTab.js component. That seems to be an empty array.
Any API call is an asynchronous action, and won't update the redux store until the promise has resolved/rejected and data has been successfully fetched. Any HTTP request takes much longer to complete than painting elements to the DOM. So your component loads with default data before being updated with the response from your API call a number of milliseconds later.
The way you've handled it with class-based components is fine. There are probably some optimizations you could add, but it should work as expected. You might even choose to render a Spinner component while you're fetching data from the API as well.
If you want a different approach using more modern React patterns, you can try and use the equivalent version with React hooks.
const Songs = ({ fetchSongs, songs, ...props }) => {
React.useEffect(() => {
// dispatch any redux actions upon mounting
// handle any component did update logic here as well
}, [songs])
// ...the rest of your component
}
Here are the docs for the useEffect hook.

Filter data according to input from child Component

I have a List of Users which I want to filter during the user Types in letters in a Textfield.
In my Child component which contains the Input field I pass the input Up via props:
onEnter(event){
console.log("ENTER")
// console.log(event.target.value)
this.props.filterEmployee(event.target.value);
}
In my Container Component I take the value
filterEmployee(val){
console.log(val)
// const allUser = this.props.allUser.allUserData;
allUser .forEach(function(user){
if(user.userNameLast.indexOf(val) != -1){
console.log(user) //works
}
});
}
The allUser is an array of data connected from my Redux-store to the Container Component.
This data are also used to display the list of Users initialzied on componentWillMount.
render() {
console.log("administration")
console.log(this.props)
const allUser = this.props.allUser.allUserData;
return (
<div id="employeeAdministration">
<h1>Mitarbeiter Verwaltung</h1>
<EmployeeAdministrationFilterList
filterEmployee={this.filterEmployee.bind(this)}
/>
{/* suchfeld - Name, Department mit checkbox*/}
<ul>
{allUser.length != 0 ? allUser.map(function (item, i) {
console.log(item)
return <li key={i}>
<UserCard
userData={item}
displayPersonalInfo={true}
showRequestDates={false}
showChangePassword={false}
/>
</li>
})
: ""
}
</ul>
</div>
)
}
}
The problem now is, that I don´t know how to tell the <UserCard /> that the data has changed. How can I pass the data from the function to the render() function?
What would be the way to go here?
EDIT:
I have also tried to go the way via the reducer, but so far it didn´t worked.
filterEmployee(val){
console.log(val)
const {dispatch} = this.props;
dispatch(filterAllUser(val));
}
And the Reducer (which is not working)
function allUser(state = {allUserData: []}, action){
switch (action.type){
case 'REQUEST_ALL_USER':
return Object.assign({}, state, {
isFetching: true
});
case 'RECEIVE_ALL_USER':
return Object.assign({}, state, {
isFetching: false,
allUserData: action.items
});
case 'FILTER_ALL_USER':
return Object.assign({}, state, {
allUserData: state.allUserData.filter(user => user.userNameLast !== action.filter )
});
default:
return state
}
}
And here is the Code how the store is connected to the component
EmployeeAdministration.PropTypes = {
allUserData: PropTypes.array
};
const mapStateToProp = state => ({
allUser: state.allUser
});
export default connect(mapStateToProp)(EmployeeAdministration)
When trying this, the result is Console output of state object
This example should be able to demonstrate a basic workflow: JSFiddle.
Basically, Redux has a one-way-dataflow. The data (here is Users in the store) is flowed from the root component to the sub-components.
Whenever you want to change the value of Users inside whichever component, you create an Action and dispatch the action to some corresponding reducer. The reducer updates the store and pass it from top to bottom.
For example, you want to filter all users whose name contains "Jo":
Action creator
Pass the Action creators into the components. An action is a plain object with format like {type: "FILTER_ALL_USERS", query: "Jo"}. Here the passing is line 73:
<Users users={this.props.users} actions={this.props.actions}></Users>
Inside the component Users, we can call this.props.actions.filter() to create an action.
Dispatch the action created
This action is automatically dispatched by redux because we have bindActionCreators in Line 93:
// Map the action creator and dispatcher
const mapDispatchToProps = (dispatch) => {
return {
actions: Redux.bindActionCreators(userActions, dispatch),
dispatch
}
}
Reducer to handle the action
All reducers will be informed about this action, but a particular one will handle it (based on its type), Line 20:
case 'FILTER_ALL_USERS':
return allUsers.filter(user => user.name.toLowerCase().indexOf(action.query.toLowerCase()) >= 0)
Re-render
The reducer will return a brand-new object as the new store, which will be passed by Redux from the root of the component. All render functions in sub-components will be called.

Categories

Resources