In React Context, how can I use state variables in state functions? - javascript

I have a React Context which looks like this:
import React, { Component } from 'react'
const AlertsContext = React.createContext({
categoryList: [],
setCategoryList: () => {}
})
export class AlertsProvider extends Component {
state = {
categoryList: [],
setCategoryList: categoryString => (
this.categoryList.includes(categoryString)
? this.setState({ categoryList: this.categoryList.filter(value => value !== categoryString) })
: this.setState({ categoryList: this.categoryList.concat([categoryString]) })
)
}
render() {
const { children } = this.props
const {categoryList, setCategoryList } = this.state
return (
<AlertsContext.Provider value={{categoryList, setCategoryList}}>
{children}
</AlertsContext.Provider>
)
}
}
export const AlertsConsumer = AlertsContext.Consumer
So, categoryList is an array of strings, each representing a category. setCategoryList should take a string; if that string is already in the array, it removes it, and if it's not in the array it adds it.
In one of my components the user can select categories from a list of checkboxes. When a checkbox is clicked, the AlertsContext setCategoryList should be called with the value of the clicked box:
import React, { Component } from 'react'
import { AlertsConsumer } from '../../../context/alerts-context'
class AlertFilters extends Component {
constructor(props) {
super(props)
this.state = {
categories: props.categories
}
}
render() {
const { categories } = this.state
return (
<AlertsConsumer>
{({ categoryList, setCategoryList }) => (
<>
{
categories.map(category => (
return (
<div key={category.id}>
<Checkbox id={category.id} value={category.value} onChange={e => setCategoryList(e.target.value)} checked={categoryList.includes(category.value)} />
<label htmlFor={category.id}>{category.value}</label>
</div>
)
))
}
</>
)}
</AlertsConsumer>
)
}
}
export default AlertFilters
This compiles ok, but when I run it and click a checkbox I get the following error:
alerts-context.jsx:77 Uncaught TypeError: Cannot read property 'includes' of undefined
This is in the line:
this.categoryList.includes(categoryString)
in the Context Provider, suggesting that "this.categoryList" is undefined at this point.
I tried changing it to
this.state.categoryList.includes(categoryString)
but it said I had to use state destructuring, so I changed to:
setCategoryList: (categoryString) => {
const { categoryList } = this.state
categoryList.includes(categoryString)
? this.setState({ categoryList: categoryList.filter(value => value !== categoryString) })
: this.setState({ categoryList: categoryList.concat([categoryString]) })
}
which highlighted the ternary operator and gave the following lint error:
Expected an assignment or function call and instead saw an expression.
What am I doing wrong?

Use if/else syntax to update the state.
setCategoryList: categoryString => {
const { categoryList } = this.state;
if (categoryList.includes(categoryString)) {
this.setState({
categoryList: categoryList.filter(value => value !== categoryString)
});
} else {
this.setState({ categoryList: categoryList.concat([categoryString]) });
}
};

Related

There is an error on the return ( and I cannot understand why

import { connect } from 'react-redux'
import { Link } from 'react-router-dom'
class MyStories extends React.Component {
addFavorite = (e) => {
this.setState({
bgcolor: "blue"
})
}
render () {
const { stories } = this.props;
const { storyBriefs } = this.props.stories.length > 0 ?
stories.map(t => (<div className="menu-inner-container"><p key={t.id}><Link to={`/stories/${t.id}`}>{t.attributes.title}</Link>
<div className="addFavoriteCss"
style={{backgroundColor: this.state.bgColor}}
onClick={this.addFavorite}> Favorite</div>
</p></div>))
//refactor - create a button that will allow for us to mark which our favorites are
return (
{ this.props.storyBriefs }
);
}
}
const mapStateToProps = state => {
return {
stories: state.myStories
}
}
export default connect(mapStateToProps)(MyStories)
getting this error
./src/components/MyStories.js
Line 26: Parsing error: Unexpected token, expected ":"
return (
^
{ this.props.storyBriefs }
);
}
I converted a functional component to a class component so that I could manipulate the state in order to change the color of the favorite button -- (I cannot use hooks or redux for the button function) Can anyone tell me what I am doing wrong?
You need to complete the ternary operator by adding :
const storyBriefs = this.props.stories.length > 0 ?
stories.map(t => (<div className="menu-inner-container"><p key={t.id}><Link to={`/stories/${t.id}`}>{t.attributes.title}</Link>
<div className="addFavoriteCss"
style={{backgroundColor: this.state.bgColor}}
onClick={this.addFavorite}> Favorite</div>
</p></div>))
: [] // you need something here after the ':', an empty array could be useful in this case
return storyBriefs
or you could shorten it to
return stories.map(t => (<div className="menu-inner-container"><p key={t.id}><Link to={`/stories/${t.id}`}>{t.attributes.title}</Link>
<div className="addFavoriteCss"
style={{backgroundColor: this.state.bgColor}}
onClick={this.addFavorite}> Favorite</div>
</p></div>))
As Jaromanda X said { this.props.storyBriefs } isn't valid. you need to provide the key value pair unless the variable doesn't have dot notation then you can define the object like that
This was the final code and it works,
import React from "react"
import { connect } from "react-redux"
import { Link } from "react-router-dom"
class MyStories extends React.Component {
constructor(props) {
super(props);
this.state = {
button: false
};
this.addFavorite = this.addFavorite.bind(this);
}
addFavorite = id => {
this.setState({
button: id
});
};
render() {
return this.props.stories.map(t => (
<div className="menu-inner-container">
<p key={t.id}>
<Link to={`/stories/${t.id}`}>{t.attributes.title}</Link>
<button
key={t.id}
className={this.state.button===t.id ? "buttonTrue" : "buttonFalse"}
onClick={() => this.addFavorite(t.id)}
>
Favorites
</button>
</p>
</div>
));
}
}
//refactor - create a button that will allow for us to mark which our favorites are
const mapStateToProps = state => {
return {
stories: state.myStories
};
};
export default connect(mapStateToProps)(MyStories);

"Cannot read property 'map' of undefined" within React, what's wrong here?

Trying to get my head around props so forgive me if its a silly mistake. I am trying to pass all of my data into one variable and pass that out into props (using {item.text} and {item.key}), however, my ".map" isn't picking up anything and there's a bunch of errors, what's wrong with my code?
The problem lays specifically here in this block of code
createList(list) {
return <li>{list.text}</li>
}
render() {
var entries = this.state.list
var finalEntries = entries.props.map(this.createList)
Here is the code in full
import React from "react";
import "./App.css";
import { isTemplateElement } from "#babel/types";
class TodoListt extends React.Component {
state = {};
constructor(props) {
super(props);
this.state = {
userInput: "",
list: [],
};
}
changeUserInput(input) {
this.setState({
userInput: input
})
}
addToList(input) {
let listArray = this.state.list;
listArray.push(input);
var newItem = {
text: listArray,
key: Date.now()
};
this.setState(prevState => {
return {
list: prevState.list.concat(newItem)
};
});
this.setState({
list: listArray
})
}
createList(list) {
return <li>{list.text}</li>
}
render() {
var entries = this.state.list
var finalEntries = entries.props.map(this.createList)
return (
<div className="to-do-list-main">
<input
onChange={(e) => this.changeUserInput(e.target.value)}
value={this.state.userInput}
type="text"
/>
<button onClick={() => this.addToList(this.state.userInput)}>Press me</button>
<ul>
{this.testingSetup()}
</ul>
</div>
);
}
}
export default TodoListt;
You can use the spread operator to add to an existing array. Simply add a new object to the array in the state, and then clear the user input, ready for another item. Based on your code, here's a simple example of adding to a state list (haven't run myself, so just check for syntax errors and such):
import React from "react";
import "./App.css";
import { isTemplateElement } from "#babel/types";
class TodoList extends React.Component {
state = {};
constructor(props) {
super(props);
this.state = {
userInput: "",
list: [],
};
}
changeUserInput(input) {
this.setState({
userInput: input
})
}
addToList() {
const { list, userInput } = this.state;
// Add item to state list using spread operator and clear input
this.setState({
list: [...list, {text:userInput, key: Date.now()}],
userInput: ""
});
}
render() {
return (
<div className="to-do-list-main">
<input
onChange={(e) => this.changeUserInput(e.target.value)}
value={this.state.userInput}
type="text"
/>
<button onClick={() => this.addToList()}>Press me</button>
<hr/>
{/* For each item in the list, render the contents */}
{this.state.list.map(item => (
<div key={item.key}>
<h3>{item.text}</h3>
<p>Time: {item.key}</p>
</div>
))}
</div>
);
}
}
export default TodoList;

Console.log() after setState() doesn't return the updated state

I've created a simple todo list to learn react and i'm trying to add some additional features. At the moment i'm trying to add buttons that toggle the list of items, so it either shows all the tasks or just those that are completed.
I've written a function to change the state of my visabilityFilter so I can later use this to toggle the items in the list, but it isn't behaving how it should be.
I console log the visabilityFilter variable but it always shows the wrong state before changing to the correct state. e.g. the 'show all' button will console log 'show completed' then if you press it again it will console log 'show all'
App.js
import React, { Component } from 'react';
import './App.css';
import TodoList from './components/TodoList.js'
import VisabilityFilter from './components/VisabilityFilter.js'
export const SHOW_ALL = 'show_all'
export const SHOW_COMPLETED = 'show_completed'
class App extends Component {
constructor (props) {
super(props)
this.state = {
inputValues: {
'newTodo': ''
},
todos: [
{
task: 'My First Todo',
completed: false
}
],
visabilityFilter: SHOW_ALL
}
this.addTodo = this.addTodo.bind(this)
this.handleInputChange = this.handleInputChange.bind(this)
this.handleKeyUp = this.handleKeyUp.bind(this)
this.toggleCompleted = this.toggleCompleted.bind(this)
this.removeTodo = this.removeTodo.bind(this)
this.checkCompleted = this.checkCompleted.bind(this)
this.setVisabilityFilter = this.setVisabilityFilter.bind(this)
}
handleInputChange (e) {
const { inputValues } = this.state
const { id, value } = e.target
this.setState({
inputValues: {
...inputValues,
[id]: value
}
})
}
handleKeyUp (e) {
var code = e.key
if(code === 'Enter') {
this.addTodo(e);
}
}
toggleCompleted (e, index) {
const { todos } = this.state
todos[index].completed = !todos[index].completed
todos.sort((a, b) => b.completed - a.completed)
this.setState({ todos })
}
removeTodo (e, index) {
const { todos } = this.state
this.setState ({ todos: todos.filter((todo, i) => i !== index) })
}
addTodo (e) {
const { todos, inputValues } = this.state
const { dataset } = e.target
if (inputValues[dataset.for] === '') return
const newTodo = { task: inputValues[dataset.for], completed: false }
todos.push(newTodo)
this.setState({
todos,
inputValues: { ...inputValues, [dataset.for]: '' }
})
}
checkCompleted (e, index) {
const { todos } = this.state
return { todos } && todos[index].completed
}
setVisabilityFilter (e) {
const { visabilityFilter } = this.state
const { dataset } = e.target
this.setState({
visabilityFilter: dataset.for
})
console.log ({ visabilityFilter })
}
render() {
const { todos, inputValues, visabilityFilter } = this.state
return (
<div className="App">
<TodoList
todos={todos}
inputValues={inputValues}
addTodo={this.addTodo}
handleInputChange={this.handleInputChange}
removeTodo={this.removeTodo}
toggleCompleted={this.toggleCompleted}
handleKeyUp={this.handleKeyUp}
checkCompleted={this.checkCompleted}
/>
<VisabilityFilter setVisabilityFilter={this.setVisabilityFilter} />
</div>
);
}
}
export default App;
VisabilityFilter.js
import React from 'react'
import { func } from 'prop-types'
import { SHOW_ALL, SHOW_COMPLETED } from '../App'
const VisabilityFilter = props => {
return (
<div>
<button data-for={SHOW_COMPLETED} onClick={ props.setVisabilityFilter } >
Show Completed Tasks
</button>
<button data-for={SHOW_ALL} onClick={ props.setVisabilityFilter }>
Show All Tasks
</button>
</div>
)
}
VisabilityFilter.propTypes = {
setVisabilityFilter: func.isRequired
}
export default VisabilityFilter
setState() is async (React docs), so the state changes won't be applied immediately. If you want to log out the new state,setState() takes in a function as the second argument and performs that function when the state is updated. So:
this.setState({
abc: xyz
},
() => console.log(this.state.abc),
)
Or you can also use componentDidUpdate(), which is recommended
In the functional components, you can use useEffect to track changes in state.
useEffect(() => {
console.log(someState);
},[someState);

React Native: Component rerender but props has not changed

I'm encountering this strange issue that I can figure out why is happing.
This should not be happening since the prop passed down to the History component has not been updated.
./components/History.js
...
const History = ({ previousLevels }) => {
return (
<ScrollView style={styles.container}>
{previousLevels.reverse().map(({ date, stressValue, tirednessValue }) => {
return (
<CardKBT
key={date}
date={date}
stressValue={stressValue}
tirednessValue={tirednessValue}
/>
)
})}
</ScrollView>
)
}
...
export default History
As can be seen in this code (below), the prop to the History is only updated once the user press Save.
App.js
import React from 'react'
import { View, ScrollView, StyleSheet } from 'react-native'
import { AppLoading, Font } from 'expo'
import Store from 'react-native-simple-store'
import { debounce } from 'lodash'
import CurrentLevels from './components/CurrentLevels'
import History from './components/History'
export default class App extends React.Component {
constructor(props) {
super(props)
this.state = {
isLoadingComplete: false,
currentLevels: {
stressValue: 1,
tirednessValue: 1,
},
previousLevels: [],
}
this.debounceUpdateStressValue = debounce(this.onChangeStressValue, 50)
this.debounceUpdateTirednessValue = debounce(
this.onChangeTirednessValue,
50
)
}
async componentDidMount() {
const previousLevels = await Store.get('previousLevels')
if (previousLevels) {
this.setState({ previousLevels })
}
}
render() {
const { stressValue, tirednessValue } = this.state.currentLevels
if (!this.state.isLoadingComplete && !this.props.skipLoadingScreen) {
return (
<AppLoading
...
/>
)
} else {
return (
<View style={{ flex: 1 }}>
<CurrentLevels
stressValue={stressValue}
onChangeStressValue={this.debounceUpdateStressValue}
tirednessValue={tirednessValue}
onChangeTirednessValue={this.debounceUpdateTirednessValue}
onSave={this.onSave}
/>
<History previousLevels={this.state.previousLevels} />
</View>
)
}
}
...
onChangeStressValue = stressValue => {
const { tirednessValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onChangeTirednessValue = tirednessValue => {
const { stressValue } = this.state.currentLevels
this.setState({ currentLevels: { stressValue, tirednessValue } })
}
onSave = () => {
Store.push('previousLevels', {
date: `${new Date()}`,
...this.state.currentLevels,
}).then(() => {
Store.get('previousLevels').then(previousLevels => {
this.setState({
currentLevels: { stressValue: 1, tirednessValue: 1 },
previousLevels,
})
})
})
}
}
The component will re-render when one of the props or state changes, try using PureComponent or implement shouldComponentUpdate() and handle decide when to re-render.
Keep in mind, PureComponent does shallow object comparison, which means, if your props have nested object structure. It won't work as expected. So your component will re-render if the nested property changes.
In that case, you can have a normal Component and implement the shouldComponentUpdate() where you can tell React to re-render based on comparing the nested properties changes.

componentWillReceiveProps this.props and nextProps always the same

I have a parent component ProductTest that holds state as the source of truth. The state is an array of variant items. Each item is simplified for this example and has a title.
When a Variant's title is updated, the child will call it's handler callback via the prop passed by the parent. The parent then updates state, and the child will then have a new title property.
What I am seeing is that the child's componentWillReceiveProps function will display the same value for this.props and nextProps. I would think that since the value is supplied by the parent, the two would be different.
I'm not sure what I'm doing incorrectly.
Here is the parent ProductTest component:
import React, { Component } from 'react'
import { connect } from 'react-redux'
import axios from 'axios'
import Variant from './variants/Variant'
import { REACT_APP_API_URL, REACT_APP_SITE_KEY } from '../../../shared/vars'
import '../../../css/variants.css'
class ProductTest extends Component {
constructor(props) {
super(props)
this.state = {
variants: []
}
this.handleVariantTitleChange = this.handleVariantTitleChange.bind(this)
this.handleVariantValueChange = this.handleVariantValueChange.bind(this)
}
componentDidMount() {
this.loadData()
}
loadData() {
const apiEndpoint = 'products/some-product'
const AUTH_HEADER = { Authorization: REACT_APP_SITE_KEY }
return axios.get(REACT_APP_API_URL + apiEndpoint, { headers: AUTH_HEADER })
.then((response) => {
this.setState({ variants: response.data.product.variants })
})
.catch((err) => {
console.log("YA GOOFED BUD!", err)
})
}
handleVariantTitleChange(e, i) {
let { variants } = this.state
const { value } = e.target
variants = variants.map((item, index) => {
item.title = i === index ? value : item.title
return item
})
this.setState({ variants })
}
handleVariantValueChange(e, id) {
let { variants } = this.state
const { value } = e.target
variants = variants.map(variant => {
variant.items = variant.items.map(item => {
item.title = id === item.id ? value : item.title
return item
})
return variant
})
this.setState({ variants })
}
render() {
return (
<div>
{this.state.variants.map((variant, i) => {
return <Variant key={i}
variant={variant}
index={i}
changeVariantTitle={this.handleVariantTitleChange}
changeVariantValue={this.handleVariantValueChange}
/>
})}
</div>
)
}
}
export default ProductTest
Here is the child Variant component:
import React, { Component } from 'react'
class Variant extends Component {
componentWillReceiveProps(nextProps){
console.log(this.props, nextProps)
}
render() {
const { variant, index } = this.props
return (
<div className="variant-wrapper" data-new="new_value">
<div className="ajax-error" data-hbs-id="{{id}}"></div>
<div className="varient-item-titles">
<div className="variant-item-title">
Title
</div>
<div className="varient-attribute-titles">
<div className="variant-attribute">Value</div>
</div>
</div>
<div className="variant-item-content">
<div className="variant-label">
<input type="text" name="variant_label" value={variant.title} placeholder="Size, colour, etc."
onChange={e => this.props.changeVariantTitle(e, index)}
/>
</div>
</div>
</div>
)
}
}
export default Variant
Before
After
Console.log()
These two objects are this.props and nextProps. Notice the title property is not different, as I'd expect them to be.
SOLUTION!
The reason for all of those is that I was not correctly altering the parent's state immutably. I think I was just changing the value at the same memory address, and hence by the time it got to the componentWillReceiveProps it was already the new value. I think...
I found that the following will work:
handleVariantTitleChange(e, i){
const { value } = e.target
// Immutably clone specific variant item
let variant = Object.assign({}, this.state.data.variants[i])
// Set new title to item
variant.title = value
// Set the variant to the cloned item
let variants = this.state.data.variants
variants[i] = variant
this.setState({
data: {
...this.state.data,
variants
}
})
}

Categories

Resources