I'm having troubles conditionally calling a mutation from a handler within my main render class. Unfortunately, I am unable to implement a submit button(limitations of project definition, my DOM inputs must dynamically render the new graph onChange) and have to verify conditions before allowing a mutation to execute, yet I seem to be unable to diagnose a fix for this!
Below, you can find the parent component code. Note that the mutation is still within the validation handler, sitting there temporarily until a fix is established.
I've also had a few of the apollo documentation tutorials pointed my way, but unfortunately they rely on a different project structure that I cannot replicate due to project limitations.
Below, you can find the parent component code. Note that the mutation is still within the validation handler, sitting there temporarily until a fix is established.
import React, { Component } from "react";
import CurrencyInput from "./CurrencyInput";
import SliderInput from "./SliderInput";
import DisplayGraph from "./DisplayGraph";
import "./InputGraphSection.css";
import FrequencyInput from "./FrequencyInput";
import { Mutation } from "react-apollo";
import gql from "graphql-tag";
const SAVINGS_MUTATION = gql`
mutation savingsmutation(
$paymentFrequency: Int!
$initialDeposit: Float!
$monthlyDeposit: Float!
$interestRate: Float!
) {
createSavings(
paymentFrequency: $paymentFrequency
initialDeposit: $initialDeposit
monthlyDeposit: $monthlyDeposit
interestRate: $interestRate
) {
savings {
months {
id
totalInterest
totalValue
}
}
}
}
`;
export default class InputGraphSectionContainer extends Component {
constructor(props) {
super(props);
this.state = {
savT: [{ x: 0, y: 0 }],
intT: [{ x: 0, y: 0 }]
};
}
handleComplete = ({ data: { createSavings } }) => {
this.setState(prevState => ({
savT: [
...prevState.savT,
// month is inside the data returned by the API????
{ x: createSavings.savings.months.id, y: createSavings.savings.months.totalValue }
],
intT: [
...prevState.intT,
{ x: createSavings.savings.months.id, y: createSavings.savings.months.totalInterest }
]
}));
};
render() {
const { savT, intT } = this.state;
return (
<Mutation mutation={SAVINGS_MUTATION} onCompleted={this.handleComplete}>
{savingsmutation => (
<InputGraphSection mutate={savingsmutation} savT={savT} intT={intT} />
)}
</Mutation>
);
}
}
class InputGraphSection extends Component {
constructor(props) {
super(props);
this.state = {
initialDeposit: "",
monthlyDeposit: "",
interestRate: 0,
paymentFrequency: ""
};
}
componentDidUpdate({ mutate }, prevState) {
console.log(this.state);
if (
this.state.initialDeposit !== "" &&
this.state.monthlyDeposit !== "" &&
this.state.paymentFrequency !== "" &&
prevState !== this.state
) {
//If currencyInput elements are returning strings, convert to ints here.
var paymentF = Number(this.state.paymentFrequency);
var initialD = parseFloat(this.state.initialDeposit);
var monthlyD = parseFloat(this.state.monthlyDeposit);
var interestR = parseFloat(this.state.interestRate)/100;
console.log("execute mutation");
mutate({
variables: {
paymentFrequency: paymentF,
initialDeposit: initialD,
monthlyDeposit: monthlyD,
interestRate: interestR
}
});
console.log("Mutation query commencing")
} else {
console.log("Input Requirements not met, will not generate graph.");
}
}
handleChange = evt => {
const { name, value } = evt.target;
this.setState({ [name]: value });
};
render() {
const {
initialDeposit,
monthlyDeposit,
interestRate,
paymentFrequency
} = this.state;
const { savT, intT } = this.props;
return (
<div>
<p className="input-label">
Inputs must be positive and have no more than 15 digits with 2 decimal
places!
</p>
<div className="financial-inputs">
<p className="input-label">What is your initial Deposit?</p>
<CurrencyInput
name="initialDeposit"
value={initialDeposit}
onInputChange={this.handleChange}
/>
<p className="input-label">How much will you save each month?</p>
<CurrencyInput
name="monthlyDeposit"
value={monthlyDeposit}
onInputChange={this.handleChange}
/>
<p className="input-label">
What is the annual interest rate you have acquired?
</p>
<SliderInput
name="interestRate"
value={Number(interestRate)}
onInputChange={this.handleChange}
/>
<p className="input-label">
Specify the frequency of interest compounding.
</p>
<FrequencyInput
name="paymentFrequency"
value={paymentFrequency}
onInputChange={this.handleChange}
/>
</div>
<div className="financial-display">
<DisplayGraph savT={savT} intT={intT} />
</div>
</div>
);
}
}
There are multiple ways you can call an apollo mutation conditionally because there are multiple ways to call a mutation in general. These ways include the Mutation component, calling mutate directly on the client, or using the graphql HOC.
Your example is using the Mutation component which follows the render prop pattern. In order to use this you need to render the component, and then call the mutation it provides:
...
render() {
return (
<Mutation
mutation={SAVINGS_MUTATION}
variables={{
paymentFrequency: paymentF,
initialDeposit: initialD,
monthlyDeposit: monthlyD,
interestRate: interestR
}}
>
{(savingsmutation, { data }) => {
return (
<CurrencyInput
value={initialDeposit}
onInputChange={() => savingsmutation()}
/>
)
}}
</Mutation>
)
}
...
You could also use the withApollo HOC to gain access to the client directly and call mutate on it.
import { withApollo } from 'react-apollo'
class InputGraphSection extends Component {
handleChange() {
this.props.client.mutate({
mutation: SAVINGS_MUTATION,
variables: {
paymentFrequency: paymentF,
initialDeposit: initialD,
monthlyDeposit: monthlyD,
interestRate: interestR
}
})
}
}
export default withApollo(InputGraphSection)
and finally using the graphql HOC
import { graphql } from 'react-apollo'
class InputGraphSection extends Component {
handleChange() {
this.props.mutate({
paymentFrequency: paymentF,
initialDeposit: initialD,
monthlyDeposit: monthlyD,
interestRate: interestR
})
}
}
export default graphql(SAVINGS_MUTATION)(InputGraphSection)
Related
I am trying to implement a condition in my react component . When the user triggers the onClick the state updates allStakes creating one array of 4 values. The problem is that I do not want the user to input more than 4 values so tried to give the limit by doing an if else statement. I tried to add a console.log in both statements.The weird fact is that setState get updated but the csonole.log is never displayed.The component keeps rendering all the values that I insert even if the array is longer than 4. Thanks in advance
import React, { Component } from 'react';
import Stake from './stake';
class FetchRandomBet extends Component {
constructor(props) {
super(props);
this.state = {
loading: true,
bet: null,
value: this.props.value,
allStakes: []
};
}
async componentDidMount() {
const url = "http://localhost:4000/";
const response = await fetch(url);
const data = await response.json();
this.setState({
loading: false,
bet: data.bets,
});
}
render() {
const { valueProp: value } = this.props;
const { bet, loading } = this.state;
const { allStakes } = this.state;
if (loading) {
return <div>loading..</div>;
}
if (!bet) {
return <div>did not get data</div>;
}
return (
< div >
{
loading || !bet ? (
<div>loading..</div>
) : value === 0 ? (
<div className="bet-list">
<ol>
<p>NAME</p>
{
bet.map(post => (
<li key={post.id}>
{post.name}
</li>
))
}
</ol>
<ul>
<p>ODDS</p>
{
bet.map(post => (
<li key={post.id}>
{post.odds[4].oddsDecimal}
<div className="stake-margin">
<Stake
onClick={(newStake) => {
if (allStakes.length <= 3) {
this.setState({ allStakes: [allStakes, ...newStake] })
console.log('stop')
} else if (allStakes.length == 4) {
console.log('more than 3')
}
}}
/>
</div>
</li>
))
}
</ul>
</div>
May be it happens because of incorrect array destructuring. Try to change this code:
this.setState({ allStakes: [allStakes, ...newStake] })
by the next one:
this.setState({ allStakes: [newStake, ...allStakes] })
Your state belongs to your FetchRandomBet component and you are trying to update that from your imported component. There are 2 solutions to that.
1> Wrap your Stake component to a separate component with onClick handler something like this
<div onClick={(newStake) => {
if (allStakes.length <= 3) {
this.setState({
allStakes: [allStakes, ...newStake
]
})
console.log('stop')
} else if (allStakes.length == 4) {
console.log('more than 3')
}
}}><Stake /></div>
Or
2> Pass the state as a prop to the Stake component which will be responsible to update the state for FetchRandomBet. something like this
<Stake parentState={this}/>
And inside the Stake component change the parentState on click of wherever you want.
I solved the problem. I transfered the onClick method in stake component and I handled the upload of the common array with an array useState. I add the value to newStake and when I click ok I retrieve newStake and spread it into a new array and then I check that array. If there is a value should not keep adding otherwise it can add values. It works fine. Thanks anyway
import React, { useState } from 'react';
import CurrencyInput from 'react-currency-input-field';
function Stake(props) {
const [newStake, setStake] = useState(null);
const [allStakes, setStakes] = useState(null);
const changeStake = (e) => {
setStake([e.target.value])
}
const mySubmit = () => {
if (!allStakes) {
setStakes([...newStake, allStakes])
props.onClick(newStake);
} else if (allStakes) {
console.log('stop')
}
}
return (
<>
<CurrencyInput
onChange={changeStake}
style={{
marginLeft: "40px",
width: "50px"
}}
placeholder="Stake"
decimalScale={2}
prefix="£"
/>
<button onClick={mySubmit}>yes</button>
<button>no</button>
{newStake}
</>
);
}
export default Stake;
I have a simple array of objects but I can't seem to update state with the filtered values. If you console.log() the filteredData variable, the data is filtering correctly. However if I use the same variable inside setState() the filtered results aren't returning when console logging the people array. Does anyone know why this is happening? I'd also like to be able to re-render the list of filtered results. Do I need to use .map() inside the setState() method?
Thanks in advance.
import React from 'react';
import ReactDOM from 'react-dom';
import { v4 as uuidv4 } from 'uuid';
class App extends React.Component {
constructor(props) {
super(props);
this.handleSearch = this.handleSearch.bind(this);
this.state = {
people: [
{ id: uuidv4(), name: 'dave' },
{ id: uuidv4(), name: 'bryan' },
{ id: uuidv4(), name: 'abi' },
{ id: uuidv4(), name: 'chris' },
],
text: ''
}
}
handleSearch(e) {
const value = e.target.value.toLowerCase()
this.setState((prevState) => ({ text: value }));
}
render() {
const { people, text } = this.state;
const filteredData = people.filter((person) => {
return person.name.toLowerCase().includes(text.toLowerCase())
})
return (
<div>
<input type="text" name="searchPeople" placeholder="Search..." onChange={ this.handleSearch } />
<ul>
{
filteredData.map((person) => (<li key={ person.id }>{ person.name }</li>))
}
</ul>
</div>
);
}
}
const root = document.querySelector('#appRoot');
ReactDOM.render(<App />, root);
Edit both setState to retain the previous state unchanged properties this way:
this.setState({
...this.state,
people: filteredData,
});
this.setState({ ...this.state, filters: { text: value } });
Like #Cybershadow mentioned in the comment above, setState is asynchronous. And your log is being triggered before the value in this.state.people changes i.e. logged the previous state value. You can use a setState callback function to make use of the changed data state after a setState update is completed. And to use the setState callback, you need to pass the callback function as an second argument to the setState() method. In your case something like this:
this.setState(
{people: filteredData},
()=>console.log(this.state.people) //callback
);
More on React's setState() method.
#mjwals as setState is non concurrent the refreshed state will not be accessible quickly, so you can compose a callback work in the setState strategy inside the callback you will get the refreshed state, so from that point you can do other activity with the refreshed information. genuinely take a look at the code underneath
import React from 'react';
import "./styles.css";
import { v4 as uuidv4 } from 'uuid';
class App extends React.Component {
constructor(props) {
super(props);
this.handleSearch = this.handleSearch.bind(this);
this.state = {
people: [
{ id: uuidv4(), name: 'dave' },
{ id: uuidv4(), name: 'bryan' },
{ id: uuidv4(), name: 'abi' },
{ id: uuidv4(), name: 'chris' }
],
text: ''
}
}
handleSearch(e) {
const value = e.target.value.toLowerCase()
this.setState({ text: value }, () => {
const { people, text } = this.state;
const filteredData = people.filter((person) => {
return person.name.toLowerCase().includes(text.toLowerCase())
})
this.setState({ people: filteredData })
});
}
render() {
const { people } = this.state;
return (
<div>
<p>Please enter a input to search</p>
<input type="text" name="searchPeople" placeholder="Search..." onChange={this.handleSearch} />
<ul>
{people.map((person) => <li key={person.id}>{person.name}</li>)}
</ul>
</div>
);
}
}
export default App;
excuse me for that probably stupid question but this is my first steps with graphql and react. I try to create component where inside is GraphQL query, and incoming props. Props is a query which should by pass into GraphQL query. I know I do something wrong but I don't know what. I add everything like client with apollo provider into my app component structure.
On a main page (index.js) I have simply layout like:
import Layout from "../components/layout"
import SearchForm from "../components/searchForm"
export default function Home() {
return (
<Layout pageTitle="React App" headerTitle="Search repositories on Github">
<SearchForm repositoryNameDefaultValue='' />
</Layout>
);
}
then I have component called searchForm:
import { Component, ChangeEvent } from "react";
import Input from "./input";
import Button from "./button";
import style from "./searchForm.module.scss";
import FindRepositoryResults from "./test";
interface IMyComponentErrors {
repositoryNameError: string;
}
interface IMyComponentProps {
repositoryNameDefaultValue: string;
}
interface IMyComponentState {
repositoryName: string;
formIsSend: boolean;
errors: IMyComponentErrors;
}
const validateForm = (errors: IMyComponentErrors): boolean => {
let valid = true;
Object.values(errors).forEach((val) => val.length > 0 && (valid = false));
return valid;
};
const validRepositoryNameRegex = RegExp(/^[A-Za-z0-9 _]*[A-Za-z0-9][A-Za-z0-9 _]*$/i);
export default class SignUpFormContainer extends Component<
IMyComponentProps,
IMyComponentState
> {
constructor(props: IMyComponentProps) {
super(props);
this.state = {
repositoryName: this.props.repositoryNameDefaultValue,
formIsSend: false,
errors: {
repositoryNameError: "",
}
};
this.handleFormSubmit = this.handleFormSubmit.bind(this);
this.handleClearForm = this.handleClearForm.bind(this);
this.handleChangeRepositoryName = this.handleChangeRepositoryName.bind(this);
}
handleChangeRepositoryName(event: ChangeEvent<HTMLInputElement>): void {
event.preventDefault();
const { value } = event.target;
let errors = this.state.errors;
if (!validRepositoryNameRegex.test(value)) {
errors.repositoryNameError = "Invalid repository name";
} else if (!value) {
errors.repositoryNameError = "Repository name is required";
} else {
errors.repositoryNameError = "";
}
this.setState({ errors, repositoryName: value });
}
handleClearForm() {
this.setState({
repositoryName: "",
formIsSend: false
});
}
handleFormSubmit(event) {
event.preventDefault();
const { repositoryName } = this.state;
let errors = this.state.errors;
if (!repositoryName) {
errors.repositoryNameError = "Repository name is required";
}
this.setState({ errors });
if (!validateForm(this.state.errors)) {
return;
} else {
this.setState({ formIsSend: true });
}
}
render() {
const { errors } = this.state;
return (
<div>
{ !this.state.formIsSend ? (
<form
aria-label="Search repositories by name"
autoComplete="off"
onSubmit={this.handleFormSubmit}
className = {style.formSearchRepository}
>
<Input
type={"text"}
title={"Repository name:"}
name={"repositoryName"}
placeholder={"Enter name of repository"}
value={this.state.repositoryName}
error={errors.repositoryNameError.length > 0}
errorMessage={errors.repositoryNameError}
onChange={this.handleChangeRepositoryName}
required
/>
<Button
onClick={this.handleFormSubmit}
title={"Search repository in Github by name"}
children={"Search"}
/>
</form>
) : <FindRepositoryResults repositoryName={this.state.repositoryName}/>}
</div>
);
}
}
and last one that more problematic where is query:
import React from "react";
import { gql, useQuery } from "#apollo/client";
const SEARCH_REPOSITORY = gql`
query findRepositories($query: String!) {
search(first: 10, query: $query, type: REPOSITORY) {
nodes {
... on Repository {
name,
owner {
login
}
primaryLanguage {
name
},
stargazers {
totalCount
},
stargazerCount,
languages(first: 20, orderBy: {field: SIZE, direction: ASC} ) {
totalCount
nodes {
name
}
},
issues {
totalCount
}
shortDescriptionHTML,
updatedAt,
watchers {
totalCount
}
}
}
}
}
`;
interface IFindRepositoryComponentProps {
repositoryName: string;
}
interface IFindRepositoryComponentState {
detailsAreOpen: boolean;
}
interface RepositoryData {
data: any;
}
interface RepositoryVars {
query: string;
}
export default class FindRepositoryResults extends React.Component<IFindRepositoryComponentProps, IFindRepositoryComponentState> {
constructor(props: IFindRepositoryComponentProps) {
super(props);
this.state = { detailsAreOpen: false };
this.showDetails = this.showDetails.bind(this);
}
showDetails() {
this.setState(state => ({
detailsAreOpen: !state.detailsAreOpen
}));
}
render() {
const { loading, data, error } = useQuery<any, RepositoryVars>(
SEARCH_REPOSITORY ,
{ variables: { query: this.props.repositoryName } }
);
return (
<section>
<h3>Results</h3>
{loading ? (
<p>Loading ...</p>
) : error ? (<p>Error {error}</p>) : (
<div>
{ data.search.nodes.length == 0 ? (<p>No results found.</p>) : data && data.search.nodes.map((repo) => (
<div>
<p>Name: {repo.name}</p>
<p>Owner: {repo.owner.login}</p>
<p>Number of stars (total): {repo.stargazerCount}</p>
<p>Primary language: {repo.primaryLanguage.name}</p>
<button onClick={this.showDetails}>{this.state.detailsAreOpen ? 'Show less' : 'Show more'}</button>
<div>
Details:
{repo.issues.totalCount}
{repo.languages.totalCount}
{repo.shortDescriptionHTML}
{repo.stargazers.totalCount}
{repo.updatedAt}
{repo.watchers.totalCount}
</div>
</div>
))}
</div>
)}
</section>
);
}
}
In this component above I made query but I don't get results. I'm not sure but is mismatching of version (DOM Rendering), I have a problem to do this correctly together with typescript, react and apollo. I'll happy if any one can show me correct way and example how this should be done. Thank you
I haven't used typescript, but React hooks and GraphQL. So you made the query but you don't get any results? If the query is executed then there should be a result or an error. If it goes that far it could help to download the Apollo-Graphql plugin (to Google Chrome perhaps?).
I would try the query in the graphi-ql playground for example.
Also, variable-name query inside of your query is a bit confusing.
Best, J
Hi I'm trying to implement search in child component , the parent component will get data from server and pass that data to child component
as props, now child component has to implement search on that data, I have used componentwillreceiveprops which is depreciated how can I implement
this without using componentwillreceiveprops, below is my code
working example on fiddle
class Parent extends React.Component{
constructor(props) {
super(props);
this.state = {
data: []
}
}
componentDidMount() {
// mimic api call
const data = [
{ key: 'react'}, { key: 'redux'},
{ key: 'javascript' }, { key: 'Ruby'} ,{key: 'angular'}
]
setTimeout(this.setState({data}), 3000);
}
render() {
return (
<React.Fragment>
<ChildComponent data = {this.state.data}/>
</React.Fragment>
)
}
}
class ChildComponent extends React.Component{
constructor(props) {
super(props);
this.state = {
data: []
}
}
componentwillreceiveprops(nextProps){
this.setState({data: nextProps.data})
}
search(e){
console.log('props,', e.target.value)
let searchedData = this.props.data.filter(el => {
return el.key.startsWith(e.target.value)
})
this.setState({data: searchedData})
};
render(){
return(
<div>
search for (react, redux, angular, ruby)
<br/> <br/> <br/>
<input type = 'text' onChange={this.search.bind(this)}/>
{this.state.data.map(d => {
return (<div key={d.key}>{d.key}</div>)
})}
</div>
)
}
}
getDerivedStateFromProps is not a direct replacement for componentWillReceiveProps. Its just meant to update state in response to any update and unlike componentWillReceiveProps, getDerivedStateFromProps is triggered on every update either from child or from parent so you cannot simply update state without any conditional check. In order to update state if the props changed, you need to store the previous props in the state of child too or update the key of child so that it triggers a re-render
There are two possible approaches to this. Below is an example of first approach with getDerivedStateFromProps
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
componentDidMount() {
// mimic api call
const data = [
{ key: "react" },
{ key: "redux" },
{ key: "javascript" },
{ key: "Ruby" },
{ key: "angular" }
];
setTimeout(() => {
this.setState({ data });
setTimeout(() => {
this.setState(prev => ({ data: [...prev.data, { key: "Golang" }] }));
}, 3000);
}, 3000);
}
render() {
return (
<React.Fragment>
<ChildComponent data={this.state.data} />
</React.Fragment>
);
}
}
class ChildComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: []
};
}
static getDerivedStateFromProps(props, state) {
if (!_.isEqual(props.data, state.prevData)) {
return {
data: props.data,
prevData: state.data
};
} else {
return {
prevData: props.data
};
}
}
search(e) {
console.log("props,", e.target.value);
let searchedData = this.props.data.filter(el => {
return el.key.startsWith(e.target.value);
});
this.setState({ data: searchedData });
}
render() {
return (
<div>
search for (react, redux, angular, ruby)
<br /> <br /> <br />
<input type="text" onChange={this.search.bind(this)} />
{this.state.data.map(d => {
return <div key={d.key}>{d.key}</div>;
})}
</div>
);
}
}
function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Parent />, rootElement);
Working DEMO
Second approach involves changing the key of the child component instead of implementing getDerivedStateFromProps
import React from "react";
import ReactDOM from "react-dom";
import _ from "lodash";
import "./styles.css";
class Parent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: [],
keyData: 0
};
}
componentDidMount() {
// mimic api call
const data = [
{ key: "react" },
{ key: "redux" },
{ key: "javascript" },
{ key: "Ruby" },
{ key: "angular" }
];
setTimeout(() => {
this.setState(prev => ({ data, keyData: (prev.keyData + 1) % 10 }));
setTimeout(() => {
this.setState(prev => ({
data: [...prev.data, { key: "Golang" }],
keyData: (prev.keyData + 1) % 10
}));
}, 3000);
}, 3000);
}
render() {
return (
<React.Fragment>
<ChildComponent data={this.state.data} key={this.state.keyData} />
</React.Fragment>
);
}
}
class ChildComponent extends React.Component {
constructor(props) {
super(props);
this.state = {
data: props.data
};
}
search(e) {
console.log("props,", e.target.value);
let searchedData = this.props.data.filter(el => {
return el.key.startsWith(e.target.value);
});
this.setState({ data: searchedData });
}
render() {
return (
<div>
search for (react, redux, angular, ruby)
<br /> <br /> <br />
<input type="text" onChange={this.search.bind(this)} />
{this.state.data.map(d => {
return <div key={d.key}>{d.key}</div>;
})}
</div>
);
}
}
function App() {
return (
<div className="App">
<h1>Hello CodeSandbox</h1>
<h2>Start editing to see some magic happen!</h2>
</div>
);
}
const rootElement = document.getElementById("root");
ReactDOM.render(<Parent />, rootElement);
Working DEMO
You can go ahead with the second method when you know that you will have quite a few updates in the child component whereas update from parent will be less frequent and the props that you have to compare getDerivedStateFromProps is nested . In such as case implementing getDerivedStateFromProps will be less performant than updating the key since you will need to perform expensive computation on each render.
To implement your componentWillReceiveProps() behavior using the new getDerivedStateFromProps() method, you can replace your current componentwillreceiveprops() hook with this:
static getDerivedStateFromProps(nextProps, state){
/* Return the new state object that should result from nextProps */
return { data : nextProps.data }
}
The getDerivedStateFromProps() will be called before your component is rendered and, if a non-null value is returned, then that return value will become the state of the component.
In your case, the state of the <ChildComponent> component has only one data field which is populated directly from props, so returning { data : nextProps.data } would be sufficent to update the data state field to match the incoming data prop.
The general idea is that you can use this method to update a component's state based on changing/incoming props.
See this documentation for more information on getDerivedStateFromProps() - hope that helps!
Update
Also on another note, it seems the way <Parent> is updating state via the setTimeout method is incorrect. You should update it as follows:
// Incorrect: setTimeout(this.setState({data}), 3000);
setTimeout(() => this.setState({data}), 3000);
I'm using a library called TinyPagination (react-pagination-custom) and I have two problems now:
The first is that apparently, Tinypagination receives a string in preKey and nextKey and I need to send two icons here. The problem is not that, because if the code is as follows:
<TinyPagination
total={count}
selectedPageId={selectedPageId}
itemPerPage={itemPerPage}
renderBtnNumber={this.renderBtnNumber}
maxBtnNumbers={maxBtnNumbers}
preKey="PREV"
nextKey="NEXT"
wrapClass="pageContainer"
btnsClass="btnsContainer"
maxBtnPerSide={2}
/>
It works perfect. However, my current code with the call to the component is the following:
<TinyPagination
total={count}
selectedPageId={selectedPageId}
itemPerPage={itemPerPage}
renderBtnNumber={this.renderBtnNumber}
maxBtnNumbers={maxBtnNumbers}
preKey={
<FontAwesomeIcon icon="angle-left" className={leftArrowPaginador} value="angle-left" />
}
nextKey={
<FontAwesomeIcon
icon="angle-right"
className="angle-right"
value="angle-right"
/>
}
wrapClass="pageContainer"
btnsClass="btnsContainer"
maxBtnPerSide={2}
/>
The first problem comes the moment I give the page "back" and then the "next" page, since a new "<" button is created every time I do the same process. (This does not happen when I send "PREV" and "NEXT" respectively)
The message in exit console(chrome) when this problem occurs is:
"Warning: Encountered two children with the same key, [object Object]. Keys should be unique so that components maintain their identity across updates. Non-unique keys may cause children to be duplicated and / or omitted - the behavior is unsupported and could change in a future version."
My original code:
import PropTypes from 'prop-types';
import React, { Component } from 'react';
import { Link } from 'react-router-dom';
import { FontAwesomeIcon } from '#fortawesome/react-fontawesome';
import './style.css';
import gql from 'graphql-tag';
import { graphql } from 'react-apollo';
import { TinyPagination } from '../../../node_modules/react-pagination-custom';
const query = gql`
query posts($first: Int) {
posts(first: $first) {
rows {
id
titulo
image_intro
category {
id
}
}
count
}
}
`;
let selectedArrow;
export class PaginatorScreen extends Component {
constructor(props) {
super(props);
this.state = { ...props };
this.changePage = this.changePage.bind(this);
this.renderBtnNumber = this.renderBtnNumber.bind(this);
this.selectedPageId = props.selectedPageId;
}
changePage = id => {
this.setState(prevState => ({ ...prevState, selectedPageId: id }));
};
buttonPageClick(id) {
selectedArrow = id;
if (typeof id.props !== 'undefined') {
selectedArrow = id.props.value;
}
const { selectedPageId } = this.state;
switch (selectedArrow) {
case 'angle-left':
this.changePage(selectedPageId - 1);
break;
case 'angle-right':
this.changePage(selectedPageId + 1);
break;
default:
this.changePage(id);
break;
}
}
renderBtnNumber(id) {
const { selectedPageId } = this.state;
return (
<button
type="button"
onClick={this.buttonPageClick.bind(this, id)}
key={id}
className={`page ${selectedPageId === id ? 'selectedPage' : ''}`}>
{id}
</button>
);
}
render() {
const { selectedPageId } = this.state;
const itemPerPage = 16;
const maxBtnNumbers = 10;
const { data } = this.props;
if (data.loading) {
return <div>Loading...</div>;
}
if (data.error) {
return <div>{data.error.message}</div>;
}
if (data.posts.rows.length <= 0) {
return <div>Nada que mostrar...</div>;
}
const {
data: {
posts: { count },
},
} = this.props;
let listShow = [...data.posts.rows];
listShow = listShow.splice((selectedPageId - 1) * itemPerPage, itemPerPage);
let { leftArrowPaginador } = 'angle-left';
leftArrowPaginador = selectedPageId === 1 ? 'angle-left-disabled' : 'angle-left';
return (
<div>
{listShow.map(i => (
<Link to={`/noticias/detalle/${i.category.id}/${i.id}/`} key={i.id}>
<h3>{i.titulo}</h3>
<img
alt={i.titulo}
src={process.env.REACT_APP_IMG_BASE + i.imagen_intro}
width={500}
/>
</Link>
))}
<TinyPagination
total={count}
selectedPageId={selectedPageId}
itemPerPage={itemPerPage}
renderBtnNumber={this.renderBtnNumber}
maxBtnNumbers={maxBtnNumbers}
preKey={
<FontAwesomeIcon icon="angle-left" className={leftArrowPaginador} value="angle-left" />
}
nextKey={
<FontAwesomeIcon
icon="angle-right"
className="angle-right"
value="angle-right"
/>
}
wrapClass="pageContainer"
btnsClass="btnsContainer"
counterStyle={{ color: 'gray' }}
spreadClass="spread-container"
spreadStyle={{ padding: '0 0px' }}
maxBtnPerSide={2}
/>
</div>
);
}
}
PaginatorScreen.propTypes = {
selectedPageId: PropTypes.number,
data: PropTypes.shape({
loading: PropTypes.bool.isRequired,
error: PropTypes.shape({ message: PropTypes.string }),
}).isRequired,
};
PaginatorScreen.defaultProps = {
selectedPageId: 2,
};
const queryOptions = {
options: props => ({
variables: {
categoryId: props.categoryId,
first: props.first,
},
}),
};
export default graphql(query, queryOptions)(PaginatorScreen);
I wish that this bug is not present when clicking on "Back" and "Next". The problem that I have detected apparently is due to the fontawesome and the switch that exists in the code because as I mentioned, when I put "NEXT" and "PREV" it works correctly.
The second result I want is, when the page is equal to one, I want the "angle-left" icon to be shown but with the className = "angle-left-disabled".
For this I made the following variable, but it does not work when I put it in preKey:
leftArrowPaginador = selectedPageId === 1? 'angle-left-disabled': 'angle-left';
The problem seems to be in your renderBtnNumber function. When it is called for the prev/next buttons, it takes an "id" of the component. It then uses this as the key.
In order to make it work, you need to check inside the function whether the "id" is a component and if it is, use a different value for key (e.g. "Prev", or "Next").