I'm using Redux Toolkit and Redux Thunk. I successfully get my data asynchronously from a data source back into my component. Inside my component, I can observe the data coming with useSelector: const booksData = useSelector((state) => state.books);
At this point, I would like to loop the books I'm getting and render my Book component for each book data I'm getting:
const renderBooks = (books) => {
books.map((book) => {
console.log(book.id);
<Book props={book}/>;
});
};
Problem is, I can see this renderBooks is called and bookIds logged for each book. The <Book> component is not rendered though.
Am I missing something obvious? Isn't the useSelector a state like useState so it should trigger rendering once updated?
Thanks in advance.
Rendering code (simplified to cut noise):
return (
<React.Fragment>
{/*some usual HTML here */}
<div>{renderBooks(booksData.books)}</div>
</React.Fragment>
);
You need to return the values:
const renderBooks = (books) => {
return books.map((book, index) => {
console.log(book.id);
return <Book key={index} props={book}/>;
});
};
Related
Problem: Component render starts to drift from actual state
Desired Output: Component render matches state.
So. I'm going to give a bit of a high-level overview with pseudocode as this issue is quite complex, and then I'll show the code.
I have a main form, and this form has an array of filter-states that are renderable in their own components. These filter-states are a one-to-many relationship with the form. The form has-many filter-states.
form: {
filters: [
filter1,
filter2
]
}
Say you want to remove an item from the state, you would do something like so in the reducer (redux)
state.form.filters.filter(f => f.id != action.payload.id)
All good. The state is updated.
Say, you want to render this state, you would do something like so:
// component code ommited, but say you get your form state from redux into the component
formState.filters.map(filter => <FilterComponent filter={filter}/>
All good. your filters are being injected into the component and everyone is happy
Now. This is where it gets weird pretty quickly.
There is a button on my FilterComponent, that says delete. This delete button goes to the reducer, runs the code to delete the filter from the formstate (as you saw above), and yes, it DOES work. The state gets updated, BUT, the UI (the array of components) starts to drift from the state. The UI shows previously deleted states, and states that should be persisted are not shown (but in the redux tab on chrome, the state is CORRECT...!)
The UI acts as if the array of states is being pop()'d; no matter how you remove the states, it will remove the final state in component render.
Now, for the code.
// This takes a list of filters from the form state and loads them into individual form components
const Filters: NextPage<Filters> = () => {
const formState = useSelector((state: any) => state.form.formState)
// In the hope that state change will force reload components, but no avail
useEffect(() => {
console.log("something has been reloaded")
}, [formState])
return (
<>
{formState.form.map((filter, i) => {
return <FilterForm defaultState={filter} key={i} index={i} />
})}
</>
);
};
export default Filters;
The form for these individual states:
Please note, this is obviously redacted a lot but the integral logic is included
const FilterForm: NextPage<FilterFormProps> = ({ defaultState, index }) => {
const formState = useSelector((state: any) => state.form.formState)
// Local component state; there are multiple forms so the state should be localised
const [FilterState, setFilterState] = useState(defaultState)
const handleDelete = (e) => {
dispatch(deleteFilter(filterState.id))
}
const updateParentState = async () => {
dispatch(updateForm(filterState))
}
useEffect(() => {
updateParentState()
}, [filterState])
return (
<CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} name={filterState.id} />
<Input
name="filter_value"
onChange={handleOnChange} // does standard jazz
value={filterState.filter_value} // standard jazz again
/>
)
}
Now what happens is this: if I click delete, redux updates the correct state, but the components display the deleted state input. Ie, take the following:
filter1: {filter_value: "one"}
filter2: {filter_value: "two"}
filter3: {filter_value: "three"}
these filters are rendered in their own forms.
Say, I click delete on filter1.
filter1 will be deleted from redux, but the UI will show two forms: one for filter1 and one for filter2.
This drift from UI to state baffles me. Obviously I am doing something wrong, can someone spot what it is?!
So, I fixed the issue.
As it turns out, there isnt really an explanation for why the above behaved as it does, but it does warrant for a better implementation.
The issue was as follows; the redux state was conflicting with the local state of the rendered components it was injected in. Why it did, is another story. Somehow, while injecting the redux state into the component and assigning it to the local state, the states went a bit haywire and drifted apart.
The solution was to get rid of the local state (filterState), the updateParentState function call and rather to update the localised state directly through the parent state that it resides in.
The new component looked something like the following:
const FilterForm: NextPage<FilterFormProps> = ({ state, index }) => {
const handleDelete = (e) => {
dispatch(deleteFilter(filterState.id))
}
const handleChange = (e) => {
dispatch(updateFormFilterState({ ...state, [e.target.name]: e.target.value }))
}
return (
<CloseButton position="absolute" right="0" top="25px" onClick={handleDelete} />
<Input
name="filter_value"
onChange={handleChange}
value={state.filter_value}
/>
)
}
Hope this answer helps someone with the same issue as me.
I'm using the react-native-wifi-reborn package to get a list of all nearby wifi points. How do I get the list out of the promise? I've looked at one solution and it uses a class, but where my app calls the function to get the list, it isn't in one.
const MainScreen = ({navigation}) => {
requestFineLocationPermission();
WifiManager.setEnabled(true);
let wifiList = WifiManager.reScanAndLoadWifiList().then((data) => {return data});
console.log(wifiList);
return (
<Layout style={styles.container}>
......
</Layout>
);
}
export default MainScreen;
When wifiList is logged, the output is {"_U": 0, "_V": 0, "_W": null, "_X": null}
This is a case of a React functional component, which is an simpler way to define a React components compared to using classes. See more info in the docs.
In your case, your component not being updated and re-rendered with the new wifiList data, because in your current code, you are not updating the component state at all:
let wifiList = WifiManager.reScanAndLoadWifiList().then((data) => {
return data
});
In this case, you are returning data result in a promise, but you are not using it to update the component state, and you aren't using await neither to retrieve the promise result. So you are actually assigning a Promise reference to wifiList, not the promise data result.
To correctly update the state on a functional component, you can use the React hook useState().
The result would look something like:
const MainScreen = ({navigation}) => {
requestFineLocationPermission();
const [wifiList, setWifiList] = useState(/* initialValue */); // you may provide an initial value (optional, defaults to undefined)
WifiManager.setEnabled(true);
WifiManager.reScanAndLoadWifiList().then((data) => {
// update the state here
setWifiList(data);
});
// this will log the initialValue 'undefined' the first time,
// then after state is is updated,
// it will log the actual wifi list data, resolved by 'WifiManager.reScanAndLoadWifiList()'
console.log(wifiList);
return (
<Layout style={styles.container}>
......
</Layout>
);
}
export default MainScreen;
My array of API generated "todo" objects.
That is console logged, but i have also saved it as a variable, todosData. Now this variable used to be the same format( array of objects with id, title, completed ) but my hardcoded data. I rendered this with components as my app is made with react. This is the code for it:
import React from "react";
import TodoItem from "./TodoItem";
import todosData from "./TodosData";
// Container for every todo object
export default function Todos() {
const todoItemArray = todosData.map((todo) => {
return <TodoItem title={todo.title} key={todo.id} completed={todo.completed} />;
});
return <div>{todoItemArray}</div>;
}
Now as you can see i havent even changed the array name when i switched from hardcoded data to api data. Both were an array of objects, as i mentioned. Just this map method is rendered 0 components to my website. Before it rendered all ( when i hardcoded the values ).
Im completely confused.
This is the fetch() method to get the data. Even though my console.log shows that isnt the problem:
let todosData = [];
fetch("https://jsonplaceholder.typicode.com/posts/")
.then((res) => res.json())
.then((data) => todosData.push(...data))
.then(() => console.log(todosData));
export default todosData;
You can't just store your data in a variable. React does not know when you mutate it. You need to use component state so that react knows when to re-render your component. Also the place to fetch data is in an effect:
export default function Todos() {
const [todos, setTodos] = useState([]);
useEffect(() => {
fetch("https://jsonplaceholder.typicode.com/posts/")
.then(res => res.json())
.then(data => setTodos(data))
}, []);
return (
<div>
{todos.map(todo => (
<TodoItem title={todo.title} key={todo.id} completed={todo.completed} />
)}
</div>;
}
I am importing some mock data which is organised as an array in productData.js. This is being passed as a prop into TableComponent. However, if conditional rendering is not used (props.productData && ...) I get Cannot read property 'map' of undefined. From my reading, this is because render is called before the props are received asynchronously.
However, with the example below, the page is re-rendered when productData is imported. I have checked in the React Developer Tool and TableComponent does contain props.ProductData which is the array just as I expected.
Why does the page not re-render with the data?
import productData from './productData.js'
...
const MainBody = () => {
return (
<TableComponent>
productData={productData}
</TableComponent>
);
}
const TableComponent = props => {
const rows = props.productData && props.productData.map((row, index) => {
return (
<tr>
<row>{row}</row>
</tr>
);
});
return <tbody>{rows}</tbody>
<TableComponent>
productData={productData}
</TableComponent>
thats a TableComponent with no props and a text inside saying productData={productData}. You probably wanted:
<TableComponent productData={productData} >
</TableComponent>
I've had major issues the last few days getting state information into the custom contentComponent of my createDrawerNavigator. I've decided to make a few pieces of content global such as user ID using the state of app.js and just passing the state as screenProps to the router like so.
App.js
state = {
score:0,
rank:0,
}
setScore = (value) => this.setState({score:value});
setRank = (value) => this.setState({rank:value});
render() {
const globalProps={
state:this.state,
setScore:this.setScore,
setRank:this.setRank
}
let RootNav = createRootNavigator();
return (
<RootNav screenProps={globalProps}></RootNav>
);
Router.js
contentComponent: ({ navigation, screenProps}) => (
<DrawerContent navigation={navigation} screenProps={screenProps}/>
),
Child.js
this.props.screenProps.setScore(5);
I'm able to access the data, but when I call to setState from the child I get the warning telling me that app.js is unmounted. My understanding was that app.js was always mounted and running because it contains your entire app? if anyone has a solution for this it'd be greatly appreciated.
I think you're getting this error because you're setting your state in the render method of your component. You would need to move it up into a lifecycle method like componentDidMount(). See the example below of your refactored code.
state = {
score:0,
rank:0,
}
setScore = (value) => this.setState({score:value});
setRank = (value) => this.setState({rank:value});
componentDidMount(){
const globalProps={
state:this.state,
setScore:this.setScore,
setRank:this.setRank
}
}
render() {
let RootNav = createRootNavigator();
return (
<RootNav screenProps={globalProps}></RootNav>
);
The componentDidMount() runs everytime the component is called or data/state changes. I think setting your state in the render() is not good practice. i havent actually tested this code but if it doesnt work you could try sending more code snippets.