Resuable component only to be re-used a certain number of times - javascript

I have part of my application that I want the user to be able to invite other users to the application, dependant on their pricing plan the number of invites they can send changes.
I am wanting to have a button 'Invite Another' that when clicked adds another InviteForm component to the current screen, but I want to stop the user inviting users one they hit their pay plan threshold?
Is this possible?
Here is my attempt,
class InviteWizard extends Component {
constructor() {
super();
this.state = {
invites: [],
threshold: 5,
filled: 1
}
}
handleAddInvite = () => {
// if (this.state.invites.length <= this.state.threshold) {
// this.state.invites.push(<InviteForm addInvite={this.handleAddInvite} />);
// }
}
componentDidMount() {
this.state.invites.push(<InviteForm addInvite={this.handleAddInvite} />);
}
render() {
return (
<div className="InviteWizard">
{this.state.invites}
</div>
)
}
}
The threshold is current hard coded for now.

First of all, you would still have to check this server-side as everything can be manually changed client-side.
I recommend that you don't use your state to store JSX elements, but instead keep the bare minimum of informations in your state, for example how many invitations the user wants to send (aka how many times he clicked on 'Invite another' + 1). Then in your render, you read that value to generate a certain number of InviteForms.
You check that the number of forms doesn't exceed the threshold in the event handler of your 'Invite another' button.

You can dynamically add your InviteForm according to invites state.
Think about a situation when a user can invite X friends,
in this case, you will be holding a state with X similar forms without any reason.
Take notice that filled === invites.length, and deciding adding new form is checking threshold - invites.length !== 0
Using Hooks:
// const threshold = 5;
function InviteWizard({ threshold }) { // threshold as props
const [invites, setInvites] = useState(1);
const handleAddInvite = () =>
threshold - invites !== 0 && setInvites(invites + 1);
return (
<div>
<button onClick={handleAddInvite}> Invite </button>
{Array.from(Array(invites), () => (
<InviteForm addInvite={handleAddInvite}/>
))}
</div>
);
}
This hooks example.
Using Class:
const threshold = 5;
class InviteWizard extends React.Component {
constructor(props) {
super(props);
this.state = { invites: 1 };
}
handleAddInvite = () => {
if (threshold - this.state.invites !== 0) {
this.setState({
invites: this.state.invites + 1
});
}
};
render() {
return (
<div>
<button onClick={this.handleAddInvite}> Invite </button>
{Array.from(
Array(this.state.invites),
() => (
<InviteForm addInvite={this.handleAddInvite}/>
)
)}
</div>
);
}
}
This Class example.
For dynamic threshold pass it using props.

Related

How to lift state properly so that I can pass my prop between components?

I have a simple flashcard app that posts information about the flashcard to the backend whenever the user clicks on their answer choice. It posts the question, the answer choices, a unique id for each flashcard, the answer that was selected, and the correct answer. The next thing I want to send to the backend is the user's name that they enter into a form at the top of the page. The problem that I'm running into is that the function that posts the data to the backend is inside my flashcard.js which structures each flash card so I can't put the form on that file because then it is on every flashcard displayed but I just want the form displayed at the top.
Here is what my flashcard.js file looks like, the data is posted in the checkGuess function:
export default function Flashcard({ flashcard }) { // recieving flashcard prop from our
mapping in flashcardlist.js, each w a unique id
const MAX_TRIES = 4
// const [incorrect, setIncorrect] = useState(incorrect)
const [guess, setGuess] = useState(0)
const [flip, setFlip] = useState(false)
const [height, setHeight] = useState('initial') //sets the state for our initial height to be
replaced by the max height
const frontEl = useRef() // lets us have a reference from the front and back through every
rerendering of them
const backEl = useRef()
// const callDouble = () =>{
// checkGuess();
// postData();
// }
async function postData() {
}
const checkGuess = (answer, stateValue) => {
try {
console.log(stateValue)
let result = fetch('http://127.0.0.1:5000/post', {
method: 'POST',
mode: 'no-cors',
headers: {
'Accept': 'application/json',
'Content-Type': 'application/json',
},
body: JSON.stringify({
key: `${Date.now()}`,
question: flashcard.question,
answer: flashcard.answer,
options: flashcard.options,
guess: answer,
user: stateValue
})
});
} catch(e) {
console.log(e)
}
if (answer === flashcard.answer) {
setFlip(true)
return
}
if (guess + 1 === MAX_TRIES) {
setFlip(true)
}
setGuess(guess + 1)
// setIncorrect(true)
}
function setMaxHeight() {
const frontHeight = frontEl.current.getBoundingClientRect().height //gives us dimensions of
the rectangle but we only need the height
const backHeight = backEl.current.getBoundingClientRect().height
setHeight(Math.max(frontHeight, backHeight, 100)) // sets the height (setHeight) to the
maximum height of front or back but the minimum is 100px
}
useEffect(setMaxHeight, [flashcard.question, flashcard.answer, flashcard.options]) //anytime any
of these change then the setMaxHeight will change
useEffect(() => {
window.addEventListener('resize', setMaxHeight) //everytime we resize our browser, it sets
the max height again
return () => window.removeEventListener('resize', setMaxHeight) //removes the eventlistener
when component destroys itself
}, [])
return (
<div
onClick={() => postData()}
className={`card ${flip ? 'flip' : ''}`} // if flip is true classname will be card and flip,
if flip isnt true it will just be card
style={{ height: height }} //setting height to the variable height
// onClick={() => setFlip(!flip)} // click changes it from flip to non flip
>
<div className="front" ref={frontEl}>
{flashcard.question}
<div className='flashcard-options'>
{flashcard.options.map(option => {
return <div key={option} onClick={() => checkGuess(option)} className='flashcard-
option'>{option}</div>
})}
</div>
</div>
<div onClick={() => setFlip(!flip)} className='back' ref={backEl}>
{flashcard.answer}
</div>
</div>
)
}
// setting the front to show the question and the answers by looping through the options to make
them each an option with a class name to style
// back shows the answer
so i've decided to render my form inside my flashcardList.js file because that is where all the cards from flashcard.js get sent and then displayed on the page in a grid like format. the form which is inside NameForm.js is rendered before the flashcards so that it appears above the flashcards. here is the flashcardlist.js file, it's very simple:
import React from 'react'
import Flashcard from './Flashcard'
export default function FLashcardList({ flashcards }) {
// taking in the flashcards as destructured props so we dont have to make a props. variable
return (
// card-grid is a container so we can put all the cards in a grid to ensure they change in size
proportionally to the size of the window //
<div className='card-grid'>
{flashcards.map(flashcard => { // loops through the flashcards api and maps each one to
flashcard
return <Flashcard flashcard={flashcard} key={flashcard.id} /> // each flashcard is then
passed down to the "Flashcard.js" component we created returned w a unique id
})}
</div>
)
}
and then here is my nameform.js file which creates the form, provides an alert to the browser displaying the name that was entered and setting the state:
import React from "react"
export default class NameForm extends React.Component {
constructor(props) {
super(props);
this.state = {value: ''};
this.handleChange = this.handleChange.bind(this);
this.handleSubmit = this.handleSubmit.bind(this);
}
handleChange = (event) => {
this.setState({value: event.target.value});
}
handleSubmit = (event) => {
alert('A name was submitted: ' + this.state.value);
this.props.checkGuess(this.props.answer, this.state.value);
event.preventDefault();
}
checkGuess() {
}
render() {
return (
<div>
<form onSubmit={this.handleSubmit}>
<label>
Name:
<input type="text" value={this.state.value} onChange={this.handleChange} />
</label>
<input type="submit" value="Submit" />
</form>
</div>
);
}
}
The form works and the alert works displaying the correct name but when i try to console log stateValue inside the checkGuess function inside flashcard.js, it returns 'undefined' in the console so I'm clearly not lifting the state correctly because the alert to the browser displays the correct name that was inserted but the console log inside the flashcard.js function is undefined. does anybody know how to solve this? id really appreciate any help. let me know if you need any other information, my backend is just one simple python file that receives the data from the post request and sends it to the database. that works fine with the old data i just need to add the user id to this data.

React Event Handling: Marking as Favorite item on Instant in

So in my recipe App, users are able to mark or unmark recipes as their favorite.
The only thing I can't wrap my head around is How to make it instant. my current code supports makes a post call to mark the recipe as favorite but you see the change of icon (i.e the filled one) they have to refresh the page.
I do need some suggestion on how can I make it work on the click.
Here is my code:
class CuisineViewById extends Component {
constructor(props) {
super(props);
this.state = {
user: {},
access_token: '',
};
this.toggleFavorite = this.toggleFavorite.bind(this);
}
componentDidMount() {
this.props.getUser(() => {
this.props.getAccessToken(this.props.user.profile.sub, () => {
console.log(this.props.user);
this.props.getCuisineById(this.props.match.params.id, this.props.accessToken);
this.props.getFavoriteRecipes(this.props.accessToken);
});
});
}
toggleFavorite(userID, recipeID, marked) {
const userpreference = {
userid: userID,
recipe: recipeID,
favorite: marked
};
axios
.post('/api/userpreference', userpreference, {
headers: {'access_token': this.props.access_token}
})
.then(res => console.log(res));
}
displayFavorite = recipeId => {
let favoriteRecipes = this.props.userPreferred;
for (var i = 0; i < favoriteRecipes.length; i++) {
if (favoriteRecipes[i].recipe === recipeId) {
return true;
} else {
}
}
};
render() {
const that = this;
const {user} = this.props;
const {cuisine} = this.props;
return (
<CuisineTileHeading
label={cuisine.label}
totalNoRecipes={cuisine.totalRecipes]}
key={cuisine.id}
>
{cuisine.recipes && cuisine.recipes.map(function(asset, index)
{
let marked = recipe.isFavorite ? 'no' : 'yes';
return (
<RecipesCards
title={recipe.title}
description={recipe.description}
chef={recipe.owner}
lastUpdated={recipe.lastUpdated}
recipeType={recipe.assetType}
key={'RecipesCard' + index}
thumbnail={recipe.thumbnailBase64}
recipeId={recipe.id}
cuisine={cuisine}
favorite={that.displayFavorite(recipe.id)}
toggleFavorite={() =>
that.toggleFavorite(userId, recipe.id, marked)
}
/>
);
})}
</CuisneTileHeading>
)
}
}
const mapStateToProps = state = ({
cuisine : state.cuisine.cuisne,
user: state.user.user,
userPreferred: state.recipe.userPrefered,
accessToken: state.asset.accessToken
)}
In my component did mount, I am calling functions to get user information, then access token and then cuisines and then user favorite recipes.
toggleFavorite is the function that makes a recipe favorite or not favorite.
displayFavorite is a function that return either true or false is recipe id matches to the recipe ID store in userpreference object.
Right now, you compute that "this recipe is favorite" from a function that returns true or false.
ReactJS has no way to automatically trigger a re-rendering of the favorite icon since it is not linked to the recipe's state at all.
If you put "isFavorite" in the recipe's state and toggle that to true or false with the onClick event, which will change the recipe's state value for "isFavorite", React should know to call a re-render on the recipe card's icon ... then you just make sure it outputs the HTML for a filled icon when true and empty icon when false. React will know to re-render all DOM elements linked to that "slice" of the state, "isFavorite recipe" in this case.
TL;DR: leverage React's state concept instead of computing if the recipe is favorited by the user through a function which does not modify the state, since re-renders are done by React when the state changes.

React - Show an array value at a time and change with the click

I have an array with 30 questions and I need to display one at a time for the student to select an answer.
I'm thinking of creating 'currentQuestion' with question 0 in componentDidMount and the moment the student clicks next I add 1 in the counter and change the state 'currentQuestion' to the next?
I wonder if there is a better solution or is this a good idea?
EDIT 1:
I have not started building yet because I do not know if my idea is good, but in this case I am displaying the 30 questions, I want to find a way to display one at a time and change when the user clicks a button.
render () {
return (
<div>
{this.state.questions.map(item => (
<p key={item.id}>
{item.component[0].content}
</p>
))}
</div>
)
}
There are many ways to achieve what you want. Since you have to wait for the student to answer your question, there is no need to loop over your questions array. You can do something like this.
class QuestionsComponent extends React.Component {
constructor (props) {
super(props);
this.state = {
questions: [
'question_1',
'question_2',
'question_3',
'question_4',
'question_5',
],
numberOfAnswers: 0,
};
}
onAnswer = () => {
this.setState({ numberOfAnswers: this.state.numberOfAnswers + 1 });
}
render = () => {
return (
<div className='CarouselTimer'>
{ this.state.questions[this.state.numberOfAnswers] }
<button onClick={ this.onAnswer }>ANSWER</button>
</div>
)
}
}
Let me know if this is what you are looking for.
I'm thinking of creating 'currentQuestion' with question 0 in componentDidMount
I guess you want to store currentQuestion in your component's state?
If so, you can initialize it in the constructor like this:
class Quizz extends Component {
constructor() {
super();
this.state = {
currentQuestion: 0
}
}
}
For the rest of the component, I think you got the right idea.
You will end up with a component looking like this:
class Quizz extends Component {
constructor() {
super();
this.state = {
questions: [],
currentQuestion: 0
}
this.nextQuestion = this.nextQuestion.bind(this)
}
componentDidMount() {
fetchQuestions().then(questions => this.setState({ questions }))
}
nextQuestion() {
this.setState(prevState => ({
currentQuestion: prevState.currentQuestion + 1
}))
}
render() {
if (!this.state.questions.length) return <div>Loading…</div>
return (
<div>
<p>{this.state.questions[this.state.currentQuestion].content}</p>
<button onClick={this.nextQuestion}>Next</button>
</div>
)
}
}

React: How can I call a method only once in the lifecycle of a component, as soon as data.loading is false

I have a React component with a prop 'total' that changes every time the component is updated:
function MyView(props) {
const total = props.data.loading ? 0 : props.data.total;
return (
<p> total </p>
);
}
The first time the component mounts the total is say 10. Every time the component is updated because of a prop change the total goes up.
Is there a way I can display the original total (in this example 10)?
I have tried setting it in this.total inside componentDidMount, but props.data.total is not yet available when componentDidMount is called. Same with the constructor. The total only becomes available when props.data.loading is false.
In order to get access to lifecycle features, you must move from function, stateless component, to a class component.
in the below example, InitialTotal is initialized in the construstor lifecycle method and it never changes.
currentTotal, is incremented each time the render function is called - when the component is re-rendered (because of props change or state changes)
it should look something like that:
class MyView extends React.Component {
constructor(props) {
super(props)
this.initialTotal = 10;
this.currentTotal = 10;
}
render() {
this.currentTotal+=1;
return (
<p>InitialToal: {this.initialTotal}</p>
<p>Current Total: {this.currentTotal}</p>
);
}
}
You could create a stateful component and store the initial total in the component state.
Example
class MyView extends React.Component {
state = {
initialTotal: this.props.total
};
render() {
const { total } = this.props;
const { initialTotal } = this.state;
return (
<div>
<p> Total: {total} </p>
<p> Initial total: {initialTotal} </p>
</div>
);
}
}
class App extends React.Component {
state = {
total: 10
};
componentDidMount() {
this.interval = setInterval(() => {
this.setState(({ total }) => {
return { total: total + 1 };
});
}, 1000);
}
componentWillUnmount() {
clearInterval(this.interval);
}
render() {
return <MyView total={this.state.total} />;
}
}
If I understand your requirements correctly...
function MyView(props) {
// if you only need to set the value once on load just use useState.
// the const total will be the value you pass in to useState.
const [total, setTotal] = useState(props.data.loading ? 0 : props.data.total)
// if its possible that the value is not available on initial load and
// you want to set it only once, when it becomes available, you can use
// useEffect with useState
useEffect(() => {
// some condition to know if data is ready to set
if (!props.data.loading) {
setTotal(props.data.total)
}
}, [props.data.total, setTotal, props.data.loading]
// this array allows you to limit useEffect to only be called when
// one of these values change. ( when total changes in this case,
// as the const function setTotal will not change
// ( but react will fuss if its used and not in the list ).
return (
<p> {total} </p>
);
}
I have the same need. With a functional component, I need to store the inital snapshot of states, let user play with different state values and see their results immediately, eventually, they can just cancel and go back to the initial states. Apply the same structure to your problem, this is how it looks:
import React from 'react';
import { useEffect, useState } from "react";
const TestView = (props: { data: any }) => {
// setting default will help type issues if TS is used
const [initialTotal, setInitialTotal] = useState(props.data.total)
useEffect(() => {
// some condition to know if data is ready to set
setInitialTotal(props.data.total);
// Critical: use empty array to ensure this useEffect is called only once.
}, [])
return (
<div>
<p> { initialTotal } </p>
<p> { props.data.total } </p>
</div>
);
}
export default TestView
You can use getDerivedStateFromProps life cycle method.
static getDerivedStateFromProps(props, state){
if(props.data.total && (props.data.total==10)){
return {
total : props.total // show total only when its 10
}
}else{
return null; // does not update state
}
}

Why does ReactJS handle radio `checked` attr differently from other attrs?

tl;dr React refuses to honor checked={checkThisOption} on inputs, even though it honors data-ischecked={checkThisOption} perfectly on the same set of inputs.
I haven't made this work on jsfiddle, but I have reproduced the issue using this code.
the long version
I've got a simple ReactJS component that presents a list of radio buttons to the user. The user is supposed to be able to pick a radio and then push a button to confirm their choice.
Here's the component def (note: I'm using ES6 & webpack):
import React from 'react';
class Widget extends React.Component {
constructor(props) {
super(props);
this.state = {
currentValue: null // tracks currently-selected choice, by its value
};
}
onClickOptionRadio = (event) => {
this.setState({
currentValue: String(event.currentTarget.value)
});
}
onConfirm = (event) => {
if(!this.props.onChange) return;
this.props.onChange(this.state.currentValue);
};
render() {
let choices = this.props.choices;
let currentValue = this.state.currentValue;
return (
<div className="Widget">
<ol className="choices">
{
choices.map((choice, i) => {
// decide whether to mark radio as checked:
// - if no current choice, check first radios
// - otherwise, check radio matching current choice
let noCurrentChoice = (currentValue === null);
let drawingFirstChoice = (i === 0);
let thisChoiceIsSelected = (String(choice.value) === currentValue);
let checkThisOption = thisChoiceIsSelected || (noCurrentChoice && drawingFirstChoice);
return (
<li key={i}>
<input type="radio" name="choices"
value={choice.value}
onChange={this.onClickOptionRadio}
checked={checkThisOption?'checked':''}
data-ischecked={checkThisOption}
/>
<label>{choice.label}</label>
{' '}
{checkThisOption ? 'CHECKED' : ''}
</li>
);
})
}
</ol>
<button onClick={this.onConfirm}>Confirm choice</button>
</div>
);
}
}
export default Widget;
Here's the owning component:
import React from 'react';
import Widget from 'components/widget';
class Owner extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let choices = [
{ value: 10, label: 'First' },
{ value: 20, label: 'Second' },
{ value: 30, label: 'Third' }
];
return (
<div className="Owner">
<Widget
choices={choices}
/>
</div>
);
}
}
export default Owner;
Here's a gif of it in action:
Note several things from the video:
the logic clearly works for checking the first radio on initial render
the other radios don't become checked when the user clicks on them
however, the logic clearly works for identifying which item is selected, as indicated by margin-right: 2rem on the radio that ought to be checked
the text indicating which option has been chosen is accurate throughout
when I click a radio, the componentWillUpdate method fires only for the Widget itself; none of its ancestors update
I think this demo proves that this isn't a case of the Widget instance being replaced by a different instance whose state is empty. The fact that the current selection is accurately reflected by a data- attr on the input, as well as plain text, shows that the state is persisting as desired. I am certain this unwanted behavior is by design, and I want to know how to work around the bizarre, exceptional logic that React applies to the form-related properties of controlled inputs.
Why do I think the current behavior is wrong? I don't want the owning component to know about each radio click -- the owner should bind to Widget's onChange method to be notified once a final choice is made.
This is a simplified example. The real component is more complicated, but the principle is the same: just as a date-picking component may have lots of internal state that the owning component is unaware of (like what time scale to show, which year, month, or week to display, etc.), so too does this component have some interesting internal state that owning components have no business managing.
As far as I can tell, I've done this exactly correctly. The component publishes its important state updates via onChange(event, newValue), which owning components should bind to. I think it's quite clear that React is deciding to not update the checked attr on these inputs, even though it's clearly capable of updating other attrs on the same elements in response to the same user actions.
Note that the owner isn't currently listening for the onChange, but that shouldn't matter: the child component should be able to manage its own internal state even when the owner isn't listening. And I reject the assertion that the radio state can't be accurate simply because Owner isn't providing a currentValue via props: Widget is plainly managing and rendering its state without that prop. React must be doing something special to prevent checked from being handled according to the rules that apply to every other element and attribute. This is an exception, and I think it's a bad one.
Finally, note that this problem only seems to occur when this component is beneath a certain comp-tree depth. When it is the only component in a Flux "page" or a Redux "container," it works great. When it's nested more deeply, it fails as I've described. I haven't yet worked out a concise way of showing that.
Any advice is appreciated. As far as I can tell, here React is violating its own stated rules, and I expect this behavior to frustrate building other stateful components that are built around vanilla inputs.
Edit: I corrected the generated names for the radios, and updated the demo to reflect it. Apologies to anyone who started chasing that stuff down.
I've edited it to not use class properties, and are not able to reproduce:
Code:
class Widget extends React.Component {
constructor(props) {
super(props);
this.state = {
currentValue: null // tracks currently-selected choice, by its value
};
this.onClickOptionRadio = this.onClickOptionRadio.bind(this)
this.onConfirm = this.onConfirm.bind(this)
}
onClickOptionRadio (event) {
this.setState({
currentValue: String(event.currentTarget.value)
});
}
onConfirm (event) {
if(!this.props.onChange) return;
this.props.onChange(this.state.currentValue);
};
render() {
let choices = this.props.choices;
let currentValue = this.state.currentValue;
return (
<div className="Widget">
<ol className="choices">
{
choices.map((choice, i) => {
// decide whether to mark radio as checked:
// - if no current choice, check first radios
// - otherwise, check radio matching current choice
let noCurrentChoice = (currentValue === null);
let drawingFirstChoice = (i === 0);
let thisChoiceIsSelected = (String(choice.value) === currentValue);
let checkThisOption = thisChoiceIsSelected || (noCurrentChoice && drawingFirstChoice);
return (
<li key={i}>
<input type="radio" name="choices"
value={choice.value}
onChange={this.onClickOptionRadio}
checked={checkThisOption?'checked':''}
data-ischecked={checkThisOption}
/>
<label>{choice.label}</label>
{' '}
{checkThisOption ? 'CHECKED' : ''}
</li>
);
})
}
</ol>
<button onClick={this.onConfirm}>Confirm choice</button>
</div>
);
}
}
class Owner extends React.Component {
constructor(props) {
super(props);
this.state = {};
}
render() {
let choices = [
{ value: 10, label: 'First' },
{ value: 20, label: 'Second' },
{ value: 30, label: 'Third' }
];
return (
<div className="Owner">
<Widget
choices={choices}
/>
</div>
);
}
}
ReactDOM.render(
<Owner />,
document.getElementById('container')
);
My browser is Chrome 47. Here is the jsfiddle.

Categories

Resources