I need your fresh eyes to help me.
I have a set of answers in my array which I shuffle on the first render.
My problem here, is that I know if i am clicking on one of the answer, the setState will re-render and consequently re-shuffle my array which i dont want.
You can have a look at my code below:
export default class extends React.Component {
constructor(props) {
super(props)
this.state = {
user: this.props.user,
token: this.props.token,
data: this.props.data,
count: 0,
select: undefined
}
this.changeQuestion = this.changeQuestion.bind(this);
this.onCorrect = this.onCorrect.bind(this);
this.onFalse = this.onFalse.bind(this);
}
static async getInitialProps({req, query}) {
const id = query.id;
const authProps = await getAuthProps(req, 'Country/Questions?theory=' + id)
return authProps
}
componentDidMount() {
if (this.state.user === undefined) {
Router.push('/login')
}
}
changeQuestion() {
this.setState({
count: this.state.count + 1,
select: undefined
})
}
onCorrect() {
this.setState({
select: true
})
}
onFalse() {
this.setState({
select: true
})
}
mixAnswers() {
const answer = this.props.data.Properties.Elements
const answers = answer[this.state.count].Properties.Answers
const answersObj = answers.reduce((ac, el, i) => {
ac.push(
<p key={i} onClick={i === 0
? this.onCorrect
: this.onFalse} className={i === 0
? 'exercices__answers--correct'
: 'exercices__answers--false'}>{el}</p>
)
return ac
}, [])
const answersShuffled = answersObj.sort(() => 0.5 - Math.random())
return answersShuffled;
}
render() {
const {user, token, data} = this.state
const answer = this.props.data.Properties.Elements
const answers = answer[this.state.count].Properties.Answers
return (
<div>
{user !== undefined
? <Layout user={this.state.user}>
<div>
{answer[this.state.count].Properties.Sources !== undefined
? <img src={answer[this.state.count].Properties.Sources[0].URL}/>
: ''}
<h1>{answer[this.state.count].Properties.Question}</h1>
{this.mixAnswers().map((el, i) => <p key={i} onClick={el.props.onClick} className={this.state.select !== undefined
? el.props.className
: ''}>{el.props.children}</p>)
}
<p>{answer[this.state.count].Properties.Description}</p>
</div>
<button onClick={this.changeQuestion}>Next Question</button>
</Layout>
: <h1>Loading...</h1>}
</div>
)
}
}
Obviously, the way I am using the 'this.mixAnswers()' method is the issue. How can I prevent it to re-render then re-shuffle this array of questions.
PS: dont pay attention about onCorrect() and onFalse().
You should make sure the logic that shuffle the answers is called only once, you can get this behavior on ComponentWillMount or ComponentDidMount, then you save them in the state of the component and in the render function instead of
{this.mixAnswers().map((el, i) => <p key={i} onClick={el.props.onClick} className={this.state.select !== undefined
? el.props.className
: ''}>{el.props.children}</p>)
}
You use this.state.answers.map()...
Related
React newbie here.
I tried to slice the data in fetchData() method and trying to access it from the render(). When I try to map the data inside render, it is throwing me a "can't read property map" error. What am I missing? I tried console.info in the fetchData() method and I'm able to see the data is being fetched correctly.
class BooksComponent extends Component{
constructor(props){
super(props)
this.state ={
booksData: [],
offset: 0,
perPage: 3,
currentPage: 0,
}
this.reserve = this.reserve.bind(this)
this.fetchData = this.fetchData.bind(this)
}
fetchData(){
axios.get('/library')
.then(res => {
const booksData = res.data
const books = booksData.slice(this.state.offset, this.state.offset + this.state.perPage)
this.setState({
pageCount: Math.ceil(booksData.length / this.state.perPage),
books })
})
}
componentDidMount() {
this.fetchData()
}
render() {
return (
<div className="App">
{this.state.books.map(book =>
<React.Fragment key={book.id}>
<p>{book.id} - {book.title} - {book.author}</p>
<button onClick={() => this.reserve(book.id)}>Reserve {book.quantity}</button>
<span>{this.state.booksData.quantity}</span>
</React.Fragment>
)}
<ReactPaginate
previousLabel={"prev"}
nextLabel={"next"}
breakLabel={"..."}
breakClassName={"break-me"}
pageCount={this.state.pageCount}
marginPagesDisplayed={2}
pageRangeDisplayed={5}
onPageChange={this.handlePageClick}
containerClassName={"pagination"}
subContainerClassName={"pages pagination"}
activeClassName={"active"}/>
</div>
)
}
reserve(id) {
console.log("clicked")
this.setState({
booksData: this.state.booksData.map(item => {
if (item.id === id) {
return { ...item, quantity: (item.quantity - 1) >= 0 ? (item.quantity - 1) : 0};
} else {
return item;
}
})
})
}
}
export default BooksComponent
In your constructor, initialize this.state.books to an empty array:
constructor(props){
super(props)
this.state ={
books: [],
booksData: [],
offset: 0,
perPage: 3,
currentPage: 0,
}
this.reserve = this.reserve.bind(this)
this.fetchData = this.fetchData.bind(this)
}
Add a conditional check inside your return statement. If this.state.books is falsey, then the second statement won't run.
{this.state.books && this.state.books.map(book =>
<React.Fragment key={book.id}>
<p>{book.id} - {book.title} - {book.author}</p>
<button onClick={() => this.reserve(book.id)}>Reserve {book.quantity}</button>
<span>{this.state.booksData.quantity}</span>
</React.Fragment>
)}
edit: also, don't forget to set books to initial state
Try below solution
{this.state.books ? {this.state.books.map(book =>
<React.Fragment key={book.id}>
<p>{book.id} - {book.title} - {book.author}</p>
<button onClick={() => this.reserve(book.id)}>Reserve
{book.quantity}</button>
<span>{this.state.booksData.quantity}</span>
</React.Fragment>
)}: null}
Also check
fetchData(){
axios.get('/library')
.then(res => {
const booksData = res.data
const books = booksData.slice(this.state.offset, this.state.offset + this.state.perPage)
console.log(book)// has data are not
this.setState({
pageCount: Math.ceil(booksData.length / this.state.perPage),
books })
})
}
I'm trying to change one value inside a nested state.
I have a state called toDoItems that is filled with data with componentDidMount
The issue is that changing the values work and I can check that with a console.log but when I go to setState and then console.log the values again it doesn't seem like anything has changed?
This is all of the code right now
export default class App extends Component {
constructor(props) {
super(props);
this.state = {
toDoItems: null,
currentView: "AllGroup"
};
}
componentDidMount = () => {
fetch("/data.json")
.then(items => items.json())
.then(data => {
this.setState({
toDoItems: [...data],
});
})
};
changeToDoItemValue = (givenID, givenKey, givenValue) => {
console.log(this.state.toDoItems);
let newToDoItems = [...this.state.toDoItems];
let newToDoItem = { ...newToDoItems[givenID - 1] };
newToDoItem.completedAt = givenValue;
newToDoItems[givenID - 1] = newToDoItem;
console.log(newToDoItems);
this.setState({
toDoItems: {newToDoItems},
})
console.log(this.state.toDoItems);
};
render() {
if (this.state.toDoItems) {
// console.log(this.state.toDoItems[5 - 1]);
return (
<div>
{
this.state.currentView === "AllGroup" ?
<AllGroupView changeToDoItemValue={this.changeToDoItemValue}/> :
<SpecificGroupView />
}
</div>
)
}
return (null)
};
}
class AllGroupView extends Component {
render() {
return (
<div>
<h1 onClick={() => this.props.changeToDoItemValue(1 , "123", "NOW")}>Things To Do</h1>
<ul className="custom-bullet arrow">
</ul>
</div>
)
}
}
So with my console.log I can see this happening
console.log(this.state.toDoItems);
and then with console.log(newToDoItems)
and then again with console.log(this.state.toDoitems) after setState
State update in React is asynchronous, so you should not expect updated values in the next statement itself. Instead you can try something like(logging updated state in setState callback):
this.setState({
toDoItems: {newToDoItems},// also i doubt this statement as well, shouldn't it be like: toDoItems: newToDoItems ?
},()=>{
//callback from state update
console.log(this.state.toDoItems);
})
I have initialized some const, lets say A, using getDerivedStateFromProps. Now I want to update the value on some action using setState but it's not working.
constructor(props) {
super(props)
this.state = {
A: []
}
static getDerivedStateFromProps(nextProps, prevState) {
const A = nextProps.A
return {
A
}
}
handleDragStart(e,data) {
e.dataTransfer.setData('item', data)
}
handleDragOver(e) {
e.preventDefault()
}
handleDrop(e, cat) {
const id = e.dataTransfer.getData('item')
const item = find(propEq('id', Number(id)), this.state.A)
const data = {
...item.data,
category: cat,
}
const val = {
...item,
data
}
this.setState({
A: item,
})
}
}
**Listing the items and Drag and Drop to Categorize**
{this.state.A.map((item, index) => (
<ListRow
key={`lm${index}`}
draggable
name={item.name ? item.name : ''}
description={item.data ? item.data.description : ''}
type={item.data ? item.data.target_types : ''}
source={item.data ? item.data.source : ''}
stars={item.data ? item.data.stars : []}
onDragStart={e => this.handleDragStart(e, item.id)}
onDragOver={e => this.handleDragOver(e)}
onDrop={e => this.handleDrop(e, 'process')}
onModal={() => this.handleToggleModal(item)}
/>
))}
I expect the value of A to be an item from HandleDrop but it's returning the same value that is loaded from getDerivedStateFromProps.
Here's how I solved this problem.
I used componentDidUpdate instead of getDerivedStatesFromProps.
componentDidUpdate(prevProps) {
if (!equals(this.props.A, prevPropsA)) {
const A = this.props.A
this.setState({
A
})
}
}
And the handleDrop function as
handleDrop(e, cat) {
const id = e.dataTransfer.getData('item')
const item = find(propEq('id', Number(id)), this.state.A)
const data = {
....data,
category: cat,
}
const val = {
...quota,
data
}
let {A} = this.state
const index = findIndex(propEq('id', Number(id)), A)
if (!equals(index, -1)) {
A = update(index, val, A)
}
this.setState({
A
})
}
Thank you very much for all of your help. Any suggestions or feedback for optimizing this sol will be highly appreciated. Thanks
I am learning ReactJS and needless to say I am an absolute beginner! I am trying to change a specific property in the array of objects which belongs to state. Every object has two properties: name and active. active values are false by default. When I click on the item, I want to change this item's active value to true.
My array is shown inside of the list element and every list element has onClick={() => props.onChangeSelected(lang.name)} method. onChangeSleceted method goes to handleChangeSelected(name) function, however, I couldn't figure out what to write inside of this function.
class Loading extends React.Component {
constructor(props) {
super(props);
this.state = {
text: 'Loading'
};
}
componentDidMount() {
const stopper = this.state.text + '...';
this.interval = window.setInterval(() => {
this.state.text === stopper
? this.setState(() => ({ text: 'Loading' }))
: this.setState((prevState) => ({ text: prevState.text + '.' }))
}, 300)
}
componentWillUnmount() {
window.clearInterval(this.interval);
}
render() {
return (
<p>
{this.state.text}
</p>
)
}
}
function LanguageList (props) {
return (
<div>
<h3>Choose your favorite:</h3>
<ul>
{props.list.map((lang) => (
<li key={lang.name} onClick={() => props.onChangeSelected(lang.name)}>
<span>{lang.name}</span>
</li>
))}
</ul>
</div>
)
}
class App extends React.Component {
constructor(props) {
super(props)
this.state = {
languages: [
{
name: 'all',
active: true
},
{
name: 'javascript',
active: false
},
{
name: 'ruby',
active: false
},
{
name: 'python',
active: false
}
]
}
this.handleChangeSelected = this.handleChangeSelected.bind(this)
}
handleChangeSelected(name) {
this.setState((currentState) => {
const lang = currentState.languages.find((lang) => lang.name === name)
return {}
})
}
render() {
return (
<div>
<LanguageList
list={this.state.languages}
onChangeSelected={this.handleChangeSelected}
/>
</div>
)
}
}
ReactDOM.render(
<App />,
document.getElementById('app')
)
</script>
You can do it in a number of ways. All you need to make sure is that you aren't mutating the original state array
handleChangeSelected(name) {
this.setState((currentState) => {
return { languages: currentState.languages.map((lang) => {
if(lang.name === name) {
return {...lang, active: true};
}
return lang;
});
})
}
Try this?
handleChangeSelected(name){
// Find matching element in state
var temp = this.state.languages;
for (var i = 0; i < temp.length; i++){
if (temp[i]["name"] === name){
temp[i]["active"] = true;
}
}
this.setState({
languages: temp
});
}
As listed in the React docs, they recommend creating a new object when calling the setState function. This is of course talking about the updater function syntax (this.setState((prevState, props) => {return {...};});), which I assume the same logic is applied to the syntax used above (passing an object into set state)
The first argument [to setState] is an updater function with the signature:
(prevState, props) => stateChange
(prevState, props) => stateChange prevState is a reference to the
previous state. It should not be directly mutated. Instead, changes
should be represented by building a new object based on the input from
prevState and props.
I can't seem to pass this handler correctly. TabItem ends up with undefined for onClick.
SearchTabs
export default class SearchTabs extends Component {
constructor(props) {
super(props)
const breakpoints = {
[SITE_PLATFORM_WEB]: {
displayGrid: true,
autoFocus: true,
},
[SITE_PLATFORM_MOBILE]: {
displayGrid: false,
autoFocus: false,
},
};
this.state = {
breakpoints,
filters: null,
filter: null,
isDropdownOpen: false,
selectedFilter: null,
tabs: null,
};
this.tabChanged = this.tabChanged.bind(this);
this.closeDropdown = this.closeDropdown.bind(this);
}
... more code
createTabs(panels) {
if(!panels) return;
const tabs = panels.member.map((panel, idx) => {
const { selectedTab } = this.props;
const { id: panelId, headline } = panel;
const url = getHeaderLogo(panel, 50);
const item = url ? <img src={url} alt={headline} /> : headline;
const classname = classNames([
searchResultsTheme.tabItem,
(idx === selectedTab) ? searchResultsTheme.active : null,
]);
this.renderFilters(panel, idx, selectedTab);
return (
<TabItem
key={panelId}
classname={classname}
idx={idx}
content={item}
onClick={this.tabChanged(idx, headline)}
/>
);
});
return tabs;
}
tabChanged(idx, headline) {
const { selectedTab } = this.props;
const { selectedFilter } = this.state;
const selectedFilterIdx = _.get(selectedFilter, 'idx', null);
if (selectedTab !== idx) {
this.props.resetNextPage();
this.props.setTab(idx, selectedFilterIdx, headline);
this.closeDropdown();
}
}
render() {
// const { panels, selectedTab } = this.props;
// if (!panels || panels.length === 0) return null;
//
//
// const { tabs, selectedTab } = this.props;
return (
<div>
<ul>{this.state.tabs}</ul>
</div>
);
}
}
export const TabItem = ({ classname, content, onClick, key }) => (
<li key={key} className={`${classname} tab-item`} onClick={onClick} >{content}</li>
);
so in TabItem onClick={onClick} ends up with undefined for onClick.
More info
here's how this used to work, when this was a function in the parent Container:
// renderDefaultTabs() {
// const { panels, selectedTab } = this.props;
//
// if (!panels || panels.length === 0) return;
//
// let filter = null;
//
// const tabs = panels.member.map((panel, idx) => {
// const { id: panelId, headline } = panel;
// const url = getHeaderLogo(panel, 50);
// const item = url ?
// <img src={url} alt={headline} /> : headline;
// const classname = classNames([
// searchResultsTheme.tabItem,
// (idx === selectedTab) ? searchResultsTheme.active : null,
// ]);
//
// filter = (idx === selectedTab) ? this.renderFilters(panel) : filter;
//
// return (
// <li
// key={panelId}
// className={classname}
// onClick={() => {
// this.tabChanged(idx, headline);
// }}
// >
// {item}
// </li>
// );
// });
So I extracted that out to that SearchTabs including moving the tabChange d method to my new SearchTabs component. And now in the container the above now does this:
renderDefaultTabs() {
const {
onFilterClick,
panels,
resetNextPage,
selectedTab,
selectedFilter,
isDropdownOpen,
} = this.props;
return (<SearchTabs
panels={panels}
...
/>);
}
Note: renderDefaultTabs() is sent as a prop to in the render() of the container and the Search calls it back thus rendering it in the Search's render():
Container
render() {
return (
<Search
request={{
headers: searchHeaders,
route: searchRoute,
}}
renderTabs={this.renderDefaultTabs}
renderSearchResults={this.renderSearchResults}
handleInputChange={({ input }) => {
this.setState({ searchInput: input });
}}
renderAltResults={true}
/>
);
}
Search is a shared component our apps use.
Update
So I mentioned that the Container's render() passes the renderDefaultTabs function as a prop to <Search />. Inside <Search /> it ultimately does this: render() { <div>{renderTabs({searchResults})}</div>} which calls the container's renderDefaultTabs function which as you can see above, ultimately renders
So it is passing it as a function. It's just strange when I click a TabItem, it doesn't hit my tabChanged function whatsoever
Update
Christ, it's hitting my tabChanged. Errr..I think I'm good. Thanks all!
onClick={this.tabChanged(idx, headline)}
This is not a proper way to pass a function to child component's props. Do it like (though it is not recommended)
onClick={() => this.tabChanged(idx, headline)}
UPDATE
I want to add more explanation. By onClick={this.tabChanged(idx, headline)}, you are executing tabChanged and pass its returned value to onClick.
With your previous implementation: onClick={() => { this.tabChanged(idx, headline); }}, now onClick will be a function similar to:
onClick = {(function() {
this.tabChanged(idx, headline);
})}
So it works with your previous implementation.
With your new implementation, onClick={() => this.tabChanged(idx, headline)} should work