pass boolean state to multiple of children - javascript

The expected
I want to put a loading state in the content of tab, whenever user clicked on the tab title to switch tab, a flag is passed down through children.
The problem
I have this App component, I fake its api call using a setTimeout
class App extends Component {
state = {
loading: false,
data: []
}
getData = () => {
return new Promise(resolve => {
return setTimeout(() => {
resolve(
[
{
id: 1,
name: "Kelas A",
list: ["Jane", "Ali", "Ahmad"]
},
{
id: 2,
name: "Kelas B",
list: ["May", "Henry", "Ben"]
}
]
)
},500)
})
}
async componentDidMount() {
this.setState({
loading: true
})
const data = await this.getData()
this.setState({
data,
loading: false
})
}
//loadingComponent = () => <div>Loading...</div>;
render() {
const { data, loading } = this.state
return (
<Tabs
activeTab={1}
loading={loading}
//loadingComponent={this.loadingComponent()}
>
{data.map(o => (
<Tab
id={o.id}
>
<Tab.Title>{o.name}</Tab.Title>
<Tab.Content>
{o.list.join(", ")}
</Tab.Content>
</Tab>
))}
</Tabs>
);
}
}
I pass loading state as prop to Tabs children component, it worked, I can see true and false:
class Tabs extends Component {
static defaultProps = {
activeTab: 1
};
static getDerivedStateFromProps(nextProps, prevState) {
if(nextProps.loading !== prevState.loading){
return {
loading: nextProps.loading
}
}
}
state = {
activeTab: this.props.activeTab
};
changeTab = tab => {
this.setState({ activeTab: tab });
};
render() {
const { children } = this.props;
const { activeTab, loading } = this.state;
console.log('true or false before pass to children', loading)
return (
<div className="tabs">
{React.Children.map(children, child =>
React.cloneElement(child, {
loading,
activeTab,
changeTab: this.changeTab
})
)}
</div>
);
}
}
But I pass that loading as prop to Tabs's children which is Tab, the loading flag became just false? I can't spot the problem.
class Tab extends Component {
static Title = ({ children, tabId, activeTab, handleTabClick }) => {
return (
<div
className={`title ${tabId === activeTab ? "active" : ""}`}
onClick={handleTabClick}
>
{children}
</div>
);
};
static Content = ({ children, tabId, activeTab, loading }) => {
loading && 'Loading...' //won't work coz loading is always false, I wonder why
return tabId === activeTab ? (
<div className="content">{children}</div>
) : null;
};
render() {
return React.Children.map(this.props.children, child =>
React.cloneElement(child, {
handleTabClick: () => this.props.changeTab(this.props.id),
tabId: this.props.id,
activeTab: this.props.activeTab,
loading: this.props.loading // why always false?
})
);
}
}
My demo
https://codesandbox.io/s/o41r35n2qz

this.props.loading is always false in your child component because it does not even get rendered when it's true, as data is empty when loading is true so data.map does not create any components.
You would need to move the loading check to a parent component that is rendered even when data is empty. Eg. https://codesandbox.io/s/xpy3r3575z
EDIT:
If you want to fetch the data separately for each tab, then you need to create separate API calls, one for fetching tab titles to render the tab headers, and one for fetching the data for the active tab.

Initially when you set it as false
state = {
loading: false,
data: []
}
When the component has been loaded you simulate it to true and then false
async componentDidMount() {
this.setState({
loading: true
})
const data = await this.getData()
this.setState({
data,
loading: false
})
}
But you never received component state changes in child components.
You can get changes of a state using componentWillReceiveProps()
So you can give it a try as
class Tab extends Component {
componentWillReceiveProps(props) { //here you will get changes whenever state changes
let loading = props.loading
this.setState({loading});
}
render() {
return React.Children.map(this.props.children, child =>
React.cloneElement(child, {
handleTabClick: () => this.props.changeTab(this.props.id),
tabId: this.props.id,
activeTab: this.props.activeTab,
loading: this.state.loading //through state
})
);
}
}

Related

Updating UI after state update in Redux

EDIT:
I fixed the problem in the reducer...changed this:
case ADD_LIST_ITEM:
return {
...state,
lists: {
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
},
loading: false
};
to this:
case ADD_LIST_ITEM:
return {
...state,
lists: [
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
],
loading: false
};
Stupid error on my part.
I have a MERN todo application using redux for state management and useEffect() for UI updates (all functional instead of class-based components). However, when I change state in the redux store, the UI does not update. This seems to only happen during an update triggered by a post request from the front end to the backend, where I pass data to an action, which is handled in a reducer (a js file rather than the useReducer() hook in this app). My backend will update properly, but the UI will crash.
What happens is, I input, say, a new list item in a given todo list, and the error I get is:
Uncaught TypeError: list.lists.map is not a function
at Dashboard (Dashboard.jsx:32)
I'm not sure where to use an additional useEffect(), if needed, or if there's a problem in my reducer...here's the relevant flow (removed all className declarations and irrelevant parts):
/* Dashboard.jsx */
// imports //
const Dashboard = ({ auth: { user }, list, getLists }) => {
useEffect(() => {
getLists();
}, [getLists]);
return (
<>
<p>Lists...</p>
{list.lists &&
list.lists.map(list => <List key={list._id} list={list} />)}
</>
);
};
Dashboard.propTypes = {
getLists: PropTypes.func.isRequired,
list: PropTypes.object.isRequired
};
const mapStateToProps = state => ({
list: state.list
});
export default connect(mapStateToProps, { getLists })(Dashboard);
/* List.jsx */
// imports
const List = ({ list, addListItem, getLists }) => {
const [text, setText] = useState('');
useEffect(() => {
getLists();
}, []);
const handleAddItem = e => {
e.preventDefault();
addListItem(list._id, { text });
setText('');
};
return (
<div>
{list.listItems &&
list.listItems.map((item, index) => (
<ListItem
key={index}
item={item}
listId={list._id}
itemIndex={index}
/>
))}
<div>
<form onSubmit={handleAddItem}>
<input
type="text"
name="text"
placeholder="add a to-do item"
value={text}
onChange={e => setText(e.target.value)}
/>
<input type="submit" value="add" />
</form>
</div>
</div>
);
};
List.propTypes = {
addListItem: PropTypes.func.isRequired,
getLists: PropTypes.func.isRequired
};
export default connect(null, {
addListItem,
getLists
})(List);
/* list.actions.js */
// imports
export const addListItem = (listId, text) => async dispatch => {
try {
const res = await api.post(`/lists/${listId}`, text); // returns all list items after adding new item
dispatch({
type: ADD_LIST_ITEM,
payload: { id: listId, data: res.data }
});
} catch (err) {
dispatch({
type: LIST_ERROR,
payload: { message: err.response.statusText, status: err.response.status }
});
}
};
/* list.reducer.js */
// imports
const initialState = {
lists: [],
list: null,
loading: true,
error: {}
};
const list = (state = initialState, action) => {
const { type, payload } = action;
switch (type) {
case GET_LISTS:
return { ...state, lists: payload, loading: false };
case LIST_ERROR:
return { ...state, error: payload, loading: false };
case ADD_LIST_ITEM:
return {
...state,
lists: {
...state.lists.map(list =>
list._id === payload.id
? { ...list, listItems: payload.data }
: list
)
},
loading: false
};
default:
return state;
}
};
export default list;
I assume when creating your app's store, you are passing list as rootReducer,
Meaning your app's main state is exactly the state list is managing.
So if you need to access property lists of the state, you need to do it like this:
const mapStateToProps = state => ({
lists: state.lists /// state in here is exactly the state of list reducer
});
Now, in Dashboard lists is that array that you manipulate in list reducer.
Also, you have defined a property also named list in list reducer. It is initially defined to be null, also in the reducer, you never change it:
const initialState = {
lists: [],
list: null, /// none of actions ever change this, meaning it's currently useless.
loading: true,
error: {}
};

Toggle specific item but not others when using map() and onClick() in React

I've created a project where I call an API to list all of the countries in the world and some facts about them. I have each country on a different card, and I want the card to say the country's name on the front and then flip to the back and show the country's continent and language when the user clicks the card. The problem is that when the user clicks the card, all of the cards flip. I realize now that I need to use id or something similar to target specific cards, but I can't figure out how.
What I've tried: There are various versions of this question on here and elsewhere, but often the code is much longer and it's harder to follow the advice, and some of those deal with changing the CSS or situations different from what I'm trying to do. I tried to create a state of 'clickedArray' to create an array that shows true of false for if any specific index is clicked, but I couldn't figure out where I could call a method that fills that array, and also I don't know if that's the correct strategy anyway.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
loading: true,
clicked: false
}
}
componentDidMount() {
fetch('https://restcountries.eu/rest/v2/all')
.then(response => response.json())
.then(json => this.setState({data: json}));
this.setState({loading: false});
}
clickHappens = () => {
this.setState({clicked: this.state.clicked ? false : true});
}
render() {
return (
<div className="container">
{this.state.data?.length > 0 && this.state.data.map((item, id) => (
<div className="box" key={id} onClick={this.clickHappens}>
{this.state.clicked === false ?
<Countryname name={item["name"]}/>
:
<Countryinformation continent={item["subregion"]} language={item["languages"][0]["name"]} />
}
</div>
))}
</div>
)
}
}
class Countryname extends React.Component {
render() {
return (
<h1>{this.props.name}</h1>
)
}
}
class Countryinformation extends React.Component {
render() {
return (
<>
<p>{this.props.continent}</p>
<p>{this.props.language}</p>
</>
)
}
}
export default App;
This is because you are using a single state value to queue all the elements from if they are clicked or not.
If you want to toggle a single card at a time, use the index to match by.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
loading: true,
clicked: null // <-- start with null state
}
}
componentDidMount() {
fetch('https://restcountries.eu/rest/v2/all')
.then(response => response.json())
.then(data => this.setState({ data }))
.finally(() => this.setState({ loading: false }));
}
clickHappens = (id) => () => {
this.setState(prevState => ({
clicked: prevState.clicked === id ? null : id, // <-- toggle back to null or to new id
}));
}
render() {
return (
<div className="container">
{this.state.data?.map((item, id) => (
<div
className="box"
key={id}
onClick={this.clickHappens(id)} // <-- pass id to toggle
>
{this.state.clicked === id ? // <-- check id match
<Countryinformation
continent={item["subregion"]}
language={item["languages"][0]["name"]}
/>
:
<Countryname name={item["name"]}/>
}
</div>
))}
</div>
)
}
}
If you want to toggle multiple then use a map object to store the ids.
class App extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
loading: true,
clicked: {} // <-- start with empty object
}
}
componentDidMount() {
fetch('https://restcountries.eu/rest/v2/all')
.then(response => response.json())
.then(data => this.setState({ data }))
.finally(() => this.setState({ loading: false }));
}
clickHappens = (id) => () => {
this.setState(prevState => ({
clicked: {
...prevState.clicked,
[id]: !prevState.clicked[id], // <-- toggle clicked boolean
},
}));
}
render() {
return (
<div className="container">
{this.state.data?.map((item, id) => (
<div
className="box"
key={id}
onClick={this.clickHappens(id)} // <-- pass id to toggle
>
{this.state.clicked[id] ? // <-- check id match
<Countryinformation
continent={item["subregion"]}
language={item["languages"][0]["name"]}
/>
:
<Countryname name={item["name"]}/>
}
</div>
))}
</div>
)
}
}

Problem with correctly rendering component with React-Transition-Group and Redux

I'm currently building my first app with React and today I encountered a problem I think I cannot resolve on my own yet.
So what's the matter:
I'm rendering a Result.js container, which consists of smaller components displaying data from the API. Initially, it is hidden (not rendered) and it gets rendered after passing query into Search component and receiving a response. I'm trying to implement a transition so it fades in (opacity 0 -> 1) after response. This is working fine, but also I want it to fade out when the user sends another request and fades in again. This is what's not working, or working parts. Right now the fade-out animation plays out, but right near the end, there is a flash of an earlier state of the component with previous data. Like there was an additional render in there. I tried different approaches like with inline styling (display: none) but most of them ended with fade-out animation not playing at all.
I'm using Redux to store API response and components' display property.
The code I've been working on can be found below. I'll be very thankful for any suggestions or insights, also related to my coding style/code 'cleanness' :) Thank you!
Result.js container:
const Result = props => {
return (
<Transition
in={props.displayResult}
timeout={1000}
mountOnEnter
unmountOnExit
>
{state => (
<div
className={`${classes.Box} ${
state === 'entering'
? classes.ResultOpen
: state === 'entered'
? classes.ResultVisible
: state === 'exiting'
? classes.ResultClosed
: state === 'exited'
? classes.ResultVisible
: null
}`}
>
<div className={classes.BoxRow}>
<Sprites />
<NameId />
</div>
<div className={classes.BoxRow}>
<div className={classes.BoxColumn}>
<Abilities />
<Metrics />
</div>
<Types />
</div>
<div className={classes.BoxRow}>
<Stats />
</div>
</div>
)}
</Transition>
);
};
const mapStateToProps = state => {
return {
displayResult: state.result.displayResult
};
};
export default connect(mapStateToProps)(React.memo(Result));
reducer.js
const initialState = {
id: null,
name: '',
spriteFront: '',
spriteBack: '',
types: [],
height: null,
weight: null,
stats: [],
baseExperience: null,
abilities: [],
moves: [],
displayResult: false,
error: false,
loading: false
};
const setResult = (state, action) => {
return updateObject(state, {
id: action.result.id,
name: action.result.name,
spriteFront: action.result.sprites.front_default,
spriteBack: action.result.sprites.back_default,
types: action.result.types,
height: action.result.height,
weight: action.result.weight,
stats: action.result.stats,
baseExperience: action.result.base_experience,
abilities: action.result.abilities,
moves: action.result.moves,
displayResult: true
});
};
const resetBox = (state, action) => {
return updateObject(state, {
displayResult: false
});
};
const fetchResultFailed = (state, action) => {
return updateObject(state, { error: true });
};
const reducer = (state = initialState, action) => {
switch (action.type) {
case actionTypes.SET_RESULT:
return setResult(state, action);
case actionTypes.FETCH_RESULT_FAILED:
return fetchResultFailed(state, action);
case actionTypes.RESET_BOX:
return resetBox(state, action);
default:
return state;
}
};
export default reducer;
actions.js
export const setResult = result => {
return {
type: actionTypes.SET_RESULT,
result: result
};
};
export const resetBox = () => {
return {
type: actionTypes.RESET_BOX
};
};
export const fetchResultFailed = () => {
return {
type: actionTypes.FETCH_RESULT_FAILED
};
};
export const nextResult = query => {
return dispatch => {
dispatch(resetBox());
setTimeout(() => {
dispatch(initResult(query));
}, 100);
};
};
export const initResult = query => {
return dispatch => {
axios
.get(`https://pokeapi.co/api/v2/pokemon/${query}`)
.then(response => {
dispatch(setResult(response.data));
console.log(response.data);
})
.catch(error => {
dispatch(fetchResultFailed());
});
};
};

State in react component won't render

My react component won't load the data from the state, at all.
My loading function works as expected, as well as the rendering for it, however, even though the state updates (I logged it, it does return the expected data) nothing with render related to it.
If posts are empty, the <p>nothing</> tag does not show, and if there is data, it's not printed in the p tag nor is it loaded into my carousel.
import React, { Component } from 'react';
import { withFirebase } from '../Firebase';
import AliceCarousel from 'react-alice-carousel';
import 'react-alice-carousel/lib/alice-carousel.css';
import PostItem from '../Market/PostItem';
class LandingPosts extends Component {
constructor(props) {
super(props);
this.state = {
text: '',
loading: false,
posts: [],
limit: 5,
};
}
componentDidMount() {
this.onListenForMessages();
}
onListenForMessages = () => {
this.setState({ loading: true });
this.props.firebase
.collectionGroup('settings')
.where('homepagepost', '==', true)
.get().then(snapshot => {
let posts = [];
snapshot.forEach(doc => {
doc.ref.parent.parent.get().then(doc => {
posts.push({ ...doc.data(), uid: doc.id });
console.log(posts);
});
});
this.setState({ posts: posts.reverse(), loading: false });
});
};
responsive = {
0: { items: 1 },
1024: { items: 3 },
};
render() {
const { loading } = this.state;
return (
<div>
{loading && <div>Loading ...</div>}
{this.state.posts && (
<p>{this.state.posts[0]}</p>
)}
{!this.state.posts && (
<p>nothing</p>
)}
<AliceCarousel
items={this.state.posts.map(item => {return <PostItem data={item}/>})}
responsive={this.responsive}
autoPlayInterval={2000}
autoPlayDirection="rtl"
autoPlay={true}
fadeOutAnimation={true}
mouseDragEnabled={true}
disableAutoPlayOnAction={true}
buttonsDisabled={true}
/>
</div>
);
}
}
export default withFirebase(LandingPosts);
I think, following code is async in in your case.
doc.ref.parent.parent.get().then(doc => {
posts.push({ ...doc.data(), uid: doc.id });
console.log(posts);
});
If so try adding setting state in then or create array of promise like this.
posts.push(
doc.ref.parent.parent.get().then(doc => {
posts.push({ ...doc.data(), uid: doc.id });
console.log(posts);
});
)
Promise.all(posts).then((_posts)=>this.setState({ posts: _posts.reverse(), loading: false });)
I think you have to "repeat" your state declaration inside render.
Like this:
const {
text,
loading,
posts,
limit
} = this.state
At least that's how I have it in my components

React Input Field logging empty string as first keystroke

I'm not sure what I'm doing wrong, but I have an input field for entering a search term and trying to filter results based on the search term. The problem is that the first value being passed is an empty string and input is offset by 1 item for each keypress after that. For example, if I type 'sea', it would update the search term to be ' se'. Then, when I try to delete the value, it is offset the other direction, so deleting ' se' ends with 's', which can't be deleted.
(Here's a link to the app in progress: https://vibrant-yonath-715bf2.netlify.com/allpokemon. The full search functionality isn't working quite yet. I'm pretty new at this.)
import React, { Component } from 'react';
import Pokemon from './Pokemon';
class PokemonList extends Component {
constructor(props) {
super(props);
this.state = {
pokemonList: [],
searchTerm: '',
fetched: false,
loading: false
};
this.updateResults = this.updateResults.bind(this);
}
componentWillMount() {
this.setState({
loading: true
});
fetch('https://pokeapi.co/api/v2/pokemon?limit=151')
.then(res => res.json())
.then(response => {
this.setState({
pokemonList: response.results,
loading: true,
fetched: true
});
});
}
handleSearchTermChange = (
event: SyntheticKeyboardEvent & { target: HTMLInputElement }
) => {
this.setState({ searchTerm: event.target.value });
this.updateResults();
};
updateResults() {
const filteredList = this.state.pokemonList.filter(
pokemon =>
pokemon.name.toUpperCase().indexOf(this.state.searchTerm.toUpperCase()) >= 0
);
console.log(this.state.searchTerm);
this.setState({
pokemonList: filteredList
});
}
render() {
const { fetched, loading, pokemonList } = this.state;
let content;
if (fetched) {
content = (
<div className="flex-grid">
{pokemonList.map((pokemon, index) => (
<Pokemon key={pokemon.name} id={index + 1} pokemon={pokemon} />
))}
</div>
);
} else if (loading && !fetched) {
content = <p> Loading ...</p>;
} else {
content = <div />;
}
return (
<div>
<input
onChange={this.handleSearchTermChange}
value={this.state.searchTerm}
type="text"
placeholder="Search"
/>
{content}
</div>
);
}
}
export default PokemonList;
setState is asynchronous, so your this.state.searchTerm is not updated when you call updateResults. You could e.g. filter the array in render instead.
Example
class App extends Component {
state = {
pokemonList: [
{ name: "pikachu" },
{ name: "bulbasaur" },
{ name: "squirtle" }
],
searchTerm: ""
};
changeSearchTerm = event => {
this.setState({ searchTerm: event.target.value });
};
render() {
const { pokemonList, searchTerm } = this.state;
const filteredList = pokemonList.filter(pokemon =>
pokemon.name.toUpperCase().includes(searchTerm.toUpperCase())
);
return (
<div>
<input value={searchTerm} onChange={this.changeSearchTerm} />
{filteredList.map(pokemon => <div>{pokemon.name}</div>)}
</div>
);
}
}
I think the problem is that you call this.updateResults();
and then calling this.setState({ searchTerm: event.target.value }); instead of using the callback function for setState.
For example:
this.setState({ searchTerm: event.target.value }, () => this.updateResults());
Hope I got it right.
Update:
Also I see many problems in your code, for example, why you update the list with a filtered list? you don't need to do that:
this.setState({
pokemonList: filteredList
});
Instead of updating the results in the state, you simply need to render the filtered list... meaning your state stay with the original list, also your filterd value, just in the render you pass the filtered list..

Categories