Issue: this.state.previousWords.map() is nor recognized as a function
I have a hangman app that is trying to render previously seen words onto the page in their own line. The issue happens when a new word is added to the array in the state previousWords: []. When the page tries to re-render this it errors stating "TypeError: this.state.previousWords.map is not a function." Below is the code snippet where the error is happening.
Based off a prior project this is correct syntax (code has been included). I need help understanding why this is not recognized and how to fix it.
<div className="col-3">
<h3>Previous words:</h3>
<br></br>
{this.state.previousWords.length ? (
<div>
{this.state.previousWords.map( (value, index) => (
<div
key={index}
className="row">
{value}
</div>
))}
</div>
):(<></>)}
</div>
Prior app with functioning .map statement:
{this.state.events.length ? (
<Dropdown.Menu>
{this.state.events.map(events => (
<Dropdown.Item
key={events.event_id}
/* TODO: onClick() */
>
{events.title}
</Dropdown.Item>
))}
</Dropdown.Menu>
) : (
<Dropdown.Menu>
<Dropdown.Item>No Events Available</Dropdown.Item>
</Dropdown.Menu>
)}
exact error
325 |
326 | <div className="col-3">
327 | <h3>Previous words:</h3>
> 328 | <br></br>
| ^ 329 | <>{console.log(this.state.previousWords)}</>
330 |
331 | { this.state.previousWords.length ? (
Setting the state for previousWord:
if( this.loss() ) {
let previousWords = [...this.state.previousWords];
previousWords.push(this.state.apiWord);
console.log("Loss: before state updated: this.state.previousWords: ", this.state.previousWords);
console.log("Loss: before state updated: let previousWords: ", previousWords);
this.setState({
previousWords: this.state.apiWord,
pageLock: true,
losses: this.state.losses + 1,
}, () => {
console.log("Loss: after setState: this.state.previousWords: ", this.state.previousWords);
this.resetGame();
setTimeout(() => {
// this.setWord();
}, 5000);
});
Console log where the state is being set. "test" was added to the initial state for testing purposes.
after API call: this.state.previousWords: ["test"]
App.js:209 Loss: before state updated: this.state.previousWords: ["test"]
App.js:210 Loss: before state updated: let previousWords: (2) ["test", "unintermitting"]
It looks like you correctly build out the new array, but then do not use it to update state:
let previousWords = [...this.state.previousWords];
previousWords.push(this.state.apiWord);
this.setState({
previousWords: this.state.apiWord, // Should be previousWords: previousWords
pageLock: true,
losses: this.state.losses + 1,
},
this.state.previousWords is not an Array that's why you're getting the error.
Easiest way to prevent it - check if this.state.previousWords is an Array Array.isArray(this.state.previousWords) and only then use map.
Array.isArray(this.state.previousWords) && this.state.previousWords.length
Related
,
I was working out with splice methods of js , but as it may seem it was not working exactly as it should remove any element from an array .
Currently its only deleting the last element from array even after providing the index value for it to delete from the array. in console.log i get the perfect output after deleting anything , but in UI part it does not update as it should , its only removing the last element from array even if i click on delete other item . How can i resolve this ?
Here's what i've tried so far :
const add_actions_options = [
{value : "Postback" , label:intl.formatMessage({ id: 'POSTBACK' })},
{value : "Uri" , label:intl.formatMessage({ id: 'URI' })}
]
const [ actions , setActions ] = useState<any | undefined>([{type : add_actions_options[0].value , label : "" , data : ""}])
const [selectOptions, setSelectOptions] = useState<any>(add_actions_options);
function addAction(){
if(actions.length < 4 ){
setSelectOptions([...add_actions_options])
setActions([...actions , {type : selectOptions[0].value , label : "" , data : ""}])
} else {
toast(intl.formatMessage({ id: 'MAX.ALLOWED.4' }), { type: "error" })
}
}
function deleteAction(index){
if(actions.length === 1 ){
toast(intl.formatMessage({ id: 'MIN.ALLOWED.1' }), { type: "error" })
} else {
const updatedFields = [...actions];
updatedFields.splice(index, 1);
console.log('index : ' , index)
console.log('updatedFields : ' , updatedFields)
setActions(updatedFields);
}
}
<div className='row my-6'>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'ACTIONS' })}*</label></h4>
<button className='btn btn-primary btn-sm btn-block' onClick={() => addAction()}>
<KTSVG path='/media/icons/duotune/arrows/arr075.svg' className='svg-icon-2' />
{intl.formatMessage({id: 'ADD.ACTION'})}
</button>
</div>
</div>
<div className='row my-6 '>
{ actions.map((item , index) => {
return(
<div key={index} className='row my-6'>
<div className='col-lg-4 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.TYPE' })}*</label></h4>
<Select
onChange={(value) => handleTypeChange(index, value)}
options={selectOptions}
/>
</div>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.LABEL' })}*</label></h4>
<input
{...formik_buttons_type.getFieldProps('action.label')}
className="form-control form-control-lg form-control-solid"
name='action.label'
id='action_label'
type="text"
maxLength={30}
onChange={(event) => handleLabelChange(index, event.target.value)}
value={actions.label}
required
onInvalid={(e) => checkLabelValidation(e)}
onInput={(e) => checkLabelValidation(e)}
/>
</div>
<div className='col-lg-3 py-2'>
<h4><label className="form-label">{intl.formatMessage({ id: 'TEMPLATE.DATA' })}*</label></h4>
<input
{...formik_buttons_type.getFieldProps('action.data')}
className="form-control form-control-lg form-control-solid"
name='action.data'
id='action_data'
type="text"
maxLength={100}
onChange={(event) => { handleDataChange(index, event.target.value); }}
value={actions.data}
required
onInvalid={(e) => checkDataValidation(e)}
onInput={(e) => checkDataValidation(e)}
/>
</div>
<div className='col-lg-2 py-2 mt-10'>
<OverlayTrigger
delay={{ hide: 50, show: 50 }}
overlay={(props) => (
<Tooltip {...props}>
{intl.formatMessage({ id: 'DEL.ACTION' })}
</Tooltip>
)}
placement="top">
<button
type='button'
style={{display: index === 0 ? 'none': 'inline-block'}}
className='btn btn-icon btn-md btn-bg-light btn-color-danger me-1'
onClick={() => deleteAction(index)}
>
<i className='fa fa-trash'></i>
</button>
</OverlayTrigger>
</div>
</div>
)
})}
</div>
I am able to receive exact index number perfect output from the logs below in deleteAction fields , but the view in browser deletes the last column(index) from the array of actions. :
console.log('index : ' , index)
console.log('updatedFields : ' , updatedFields)
can anyone help me with this ?
code sand box : https://codesandbox.io/s/vibrant-christian-bktnot
Thanks and Regards !
Whenever using the index as a key for an element. We have to ensure we are not modifying the state array to avoid bugs. If you are modifying as #Dave suggested use unique keys.
The problem here is using the index as key, When we remove an element from an array react compares the previous keys [0,1,2,3] with new keys [0,1,2].
If you notice closely, Even if we remove index (1) using splice(1,1) method. The elements which are rendered again have starting index of 0.
React compares keys previous keys [0,1,2,3] with new keys [0,1,2] and finds out that index 3 is removed hence it every time removes the 3rd element in the above example (or the last index) from DOM. However, your state is reflecting the correct array element.
To avoid this use a unique key.
Codesandbox for a working example.
If you are not having keys in objects, To generate unique keys we can use one of the following as per your use case:
crypto.randomUUID();
Date.now().toString(36) + Math.random().toString(36).substr(2)
https://reactjs.org/docs/lists-and-keys.html
We don’t recommend using indexes for keys if the order of items may change. This can negatively impact performance and may cause issues with component state.
When you do
{ actions.map((item , index) => {
return(
<div key={index}
you are pretty much asking for issues with component state. When react is trying to determine which element in the UI to update, and it sees "the array with a length of 6 now has a length of 5, which element do I need to remove", it will find the one with a key that no longer exists, in this case the one at the end since you used index as key, and will remove it.
I would probably do
function randomId(){
const uint32 = window.crypto.getRandomValues(new Uint32Array(1))[0];
return uint32.toString(16);
}
const [ actions , setActions ] = useState<any | undefined>([{id: randomId(), type : add_actions_options[0].value , label : "" , data : ""}])
function addAction(){
if(actions.length < 4 ){
setSelectOptions([...add_actions_options])
setActions([...actions , {id: randomId(), type : selectOptions[0].value , label : "" , data : ""}])
} else {
toast(intl.formatMessage({ id: 'MAX.ALLOWED.4' }), { type: "error" })
}
}
{ actions.map((item) => {
return(
<div key={item.id}
where randomId can be anything that gives you a unique ID, or (even better) if there is an existing property on the actual data that uniquely identifies it, you could use that.
I'm using react testing library and I want to test a button which is :
<button data-testid="btn" onClick={() => props.history.push(`/post/${value}`)} > Search </button>
<input data-testid="input" type="text" value={value} onChange={(e) => handle(e.target.value)} />
and I test it's onClick function like this :
test('Button should navigate the url when enabled', () => {
render(<Home />);
const input = screen.getByTestId('input');
const btn = screen.getByTestId('btn');
fireEvent.change(input, { target: { value: '12' } });
fireEvent.click(btn);
});
But it gives me an error :
TypeError: Cannot read property 'push' of undefined
19 | <button
20 | data-testid="btn"
> 21 | onClick={() => props.history.push(`/post/${value}`)}
| ^
22 | disabled={!value}
23 | type="submit"
24 | >
The app itself works fine when I npm start but fails only when testing it .
How can I solve this push of undefined ?
You have to pass props to the rendered component, in this case - <Home />:
import history from 'history' // Or a file you made that contains an instance of history
test('Button should navigate the url when enabled', () => {
render(<Home history={history} {...otherProps} />);
const input = screen.getByTestId('input');
const btn = screen.getByTestId('btn');
fireEvent.change(input, { target: { value: '12' } });
fireEvent.click(btn);
});
I'm following Traversey Media's React crash course, and I wanted to extend what he built to save TodoItems to a Firebase realtime database. The TodoItems save just fine, but do not appear in the UI.
I've tried having the app start with a hard-coded TodoItem in case it was an issue of having an empty array, but this does not help anything.
relevant methods:
(full code available at https://github.com/etothepi16/react-todo)
// Delete todo item
delTodo = (id) => {
fire.database().ref(`todos/${id}`).remove()
.then(function(){
this.setState({
// Loop through todos array and filter out item with provided ID
// ... is the spread operator, used here to copy the todos array
todos: [...this.state.todos.filter((todo) => todo.id !== id)]
})
.catch(function(error){
console.log("Remove failed: " + error.message);
})
});
};
// Add todo
addTodo = (title) => {
let id = uuid.v4();
let database = fire.database();
let todosRef = database.ref(`todos/${id}`);
let newTodo = {
id: this.id,
title: title,
completed: false
}
todosRef.set(newTodo).then(
this.setState({todos: [...this.state.todos, newTodo]})
);
};
render() {
return (
<Router>
<div className='App'>
<div className='container'>
<Header />
<Route
exact
path='/'
render={(props) => (
<React.Fragment>
<AddTodo addTodo={this.addTodo} />
<Todos
todos={this.state.todos}
markComplete={this.markComplete}
delTodo={this.delTodo}
/>
</React.Fragment>
)}
/>
<Route path='/about' component={About} />
</div>
</div>
</Router>
);
}
};
class Todos extends Component {
render(){
return this.props.todos.map((todo)=>(
<TodoItem todo={todo} markComplete={this.props.markComplete} delTodo={this.props.delTodo}/>
));
}
}
TodoItem.js
render() {
const { id, title } = this.props.todo;
return (
<div style={this.getStyle()}>
<p>
<input type="checkbox" onChange={this.props.markComplete.bind(this, id)} />{' '}
{ title }
<button className="delete" style={btnStyle} onClick={this.props.delTodo.bind(this,id)}>x</button>
</p>
</div>
)
}
}
No TodoItems show up in the UI as stated before.
Error message:
Warning: Failed prop type: The prop todos is marked as required in TodoItem, but its value is undefined.
in TodoItem (at Todos.js:8)
in Todos (at App.js:87)
in Route (at App.js:81)
in div (at App.js:79)
in div (at App.js:78)
in Router (created by BrowserRouter)
in BrowserRouter (at App.js:77)
in App (at src/index.js:4)
Error I get when I try to add a new TodoItem:
AddTodo error
Error: Reference.set failed: First argument contains undefined in property 'todos.7cda085d-7653-4895-a140-d6f2629af9ca.id'
C:/Users/Paul/Desktop/react-todo/src/App.js:70
67 | title: title,
68 | completed: false
69 | }
> 70 | todosRef.set(newTodo).then(
^ 71 | this.setState({todos: [...this.state.todos, newTodo]})
72 | );
73 | };
262 |
263 | this.onSubmit = e => {
264 | e.preventDefault();
> 265 | this.props.addTodo(this.state.title);
266 | this.setState({
267 | title: ''
268 | });
onClick={this.props.delTodo.bind(this,id)}
You don't need to bind this here.
You just need to create an anonymous function and call your markComplete and delTodo function with given id in that anonymous function,
render() {
const { id, title } = this.props.todo;
return (
<div style={this.getStyle()}>
<p>
<input type="checkbox" onChange={() => this.props.markComplete(id)} />{' '}
{ title }
<button className="delete" style={btnStyle} onClick={() => this.props.delTodo(id)}>x</button>
</p>
</div>
)
}
}
I am working on a project and i want to display a hidden <div> below another <div> element using an event handler but when i click the icon that is meant to display the div, the whole page becomes blank
This is image I want:
This is what i get
I have tried to check through the internet for some places where i could get the solution. Well i found something similar to what i had done but the error still happens for me.
class PostItTeaser extends Component {
state = {
postIt: false,
moreIt: false,
}
togglePostIt = e => {
e ? e.preventDefault() : null
this.setState({ postIt: !this.state.postIt })
}
_toggle = e => {
e ? e.preventDefault() : null
this.setState({
moreIt: !this.state.moreIt,
})
}
Child = () => <div className="modal">Hello, World!</div>
render() {
let { postIt } = this.state
let { moreIt } = this.state
let {
type,
group,
disabled,
session: { id, username },
} = this.props
return (
<div>
<div
className="post_it inst"
style={{ marginBottom: type == 'group' && 10 }}
>
<img src={`/users/${id}/avatar.jpg`} alt="Your avatar" />
<div className="post_teaser">
<span
className="p_whats_new"
onClick={disabled ? null : this.togglePostIt}
>
What's new with you, #{username}? #cool
</span>
<span className="m_m_exp" data-tip="More" onClick={this._toggle}>
<MaterialIcon icon="expand_more" />
</span>
</div>
</div>
{moreIt && <Child />}
{postIt && (
<PostIt back={this.togglePostIt} type={type} group={group} />
)}
</div>
)
}
}
From skimming through the code I believe you need to bind the scope, since the function you're calling is using this.setState, it needs this to be the react component, not the event you're listening to:
onClick={this._toggle.bind(this)}
You can also bind the functions scope in the constructor. Or, a less memory performant & ugly way:
onClick={() => { this._toggle(); } }
I have a problem with this code:
<InfiniteScroll
dataLength={beers.length}
next={fetchMoreBeers}
hasMore={true}
loader={<p>Loading ...</p>}
endMessage={<p id="beers-end">No more beers :(</p>}
>
{beers.map((beer, index) => (
<div key={index}>
<Link
to={{
pathname: `/beer/${beer.id}`,
state: { beer: beer.id },
}}
>
<div className="beer-wrapper">
<div className="beer">
<img
className="beer-img"
src={beer.image_url}
alt={beer.name}
/>
<p className="beer-name">
{beer.name.length < 15
? `${beer.name}`
: `${beer.name.substring(0, 20)}...`}
</p>
<p className="beer-tagline">
{beer.tagline.length < 20
? `${beer.tagline}`
: `${beer.tagline.substring(0, 25)}...`}
</p>
</div>
</div>
</Link>
<Route path="/beer/:id" component={Beer} />
</div>
))}
</InfiniteScroll>;
And when the page is scrolled the error occurs:
"TypeError: Cannot read property 'id' of undefined"
for this line:
pathname: `/beer/${beer.id}`,
This seems like InfiniteScroll doesn't see the data from map() function ....
Maybe some of you know how to fix this problem?
Thanks for any tips!
Perhaps the issue is with template literals. When we add template literals directly it throws such issues. I experienced the same in my projects.
May be you can try in below way
render(){
const { beers } = this.props;
const beerItems = beers.map((beer, index) => {
let pathName = `/beer/${beer.id}`;
return (
<div key={index}>
<Link to={{
pathname: {pathName},
state: {"beer": beer.id}
}} >
<div className="beer-wrapper">
<div className="beer">
<img className="beer-img" src={beer.image_url} alt={beer.name} />
<p className="beer-name">
{beer.name.length < 15 ? beer.name : beer.name.substring(0, 20) }
</p>
<p className="beer-tagline">
{beer.tagline.length < 20 ? beer.tagline : beer.tagline.substring(0, 25) }
</p>
</div>
</div>
</Link>
<Route path="/beer/:id" component={Beer} />
</div>
)
}
return(
<InfiniteScroll
dataLength={beers.length}
next={fetchMoreBeers}
hasMore={true}
loader={<p>Loading ...</p>}
endMessage={<p id="beers-end">No more beers :(</p>}
>
{beerItems}
</InfiniteScroll>
)
}
Maybe there is an undefined value stored in beers array. Try to filter them out, like so:
beers.filter(Boolean).map((beer, index) => (
// ...
)
Also, I've noticed that hasMore prop is always set to true. To see if it helps, try something like:
<InfiniteScroll
dataLength={beers.length}
next={fetchMoreBeers}
hasMore={beers.length < 25} // or hasMore={this.state.hasMore}
loader={<p>Loading ...</p>}
endMessage={<p id="beers-end">No more beers :(</p>}
> ...
EDIT:
You are using Array.from({ length: 20 })). The output array will contain 20 undefined values. That is why you should consider beers.filter(Boolean).
I suppose this is what you really meant:
this.setState({
items: this.state.beers.concat(Array.from(this.state.beers))
})
or:
this.setState({
items: [...this.state.beers, ...this.state.beers]
})
You should check for the status code when fetching the beers. fetch will still let you do your thing even if the request failed. If you exceed your endpoint data limit, you'll get this data added to your beer array:
{
"statusCode": 400,
"error": "Bad Request",
"message": "Invalid query params",
"data": [{
"param": "per_page",
"msg": "Must be a number greater than 0 and less than 80",
"value": "100"
}]
}
As you can see there's no id param there which makes this strange error to show up.
Including a json schema validator (like https://ajv.js.org/) to make sure the data is in expected format would be a perfect choice.
I think that this problem is because of data records limitations in this Api .