How to use Tinypaginator with fontawesome in ReactJS? - reactjs

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").

Related

if else statement not working in react component

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;

Problem with make query in correct way using typescript, react and graphql

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

React-Router: How do I add a new component and route to the onboarding steps on a wizard?

This project I am working with has an onboarding Wizard, basically some code to deal with the step by step onboarding process almost similar to what you see here:
https://medium.com/#l_e/writing-a-wizard-in-react-8dafbce6db07
except this one supposedly has a function to convert a component or step into a route:
convertStepToRoute = step => {
const Component = StepComponents[step.component || ''];
return Component
? <Route
key={step.key}
path={`${WizardLayout.pathname}/${step.url}`}
render={this.renderRouteComponent(Component)}
/>
: null;
};
StepComponents comes from import StepComponents from '../Steps'; which is a directory with all the components, they were six now seven of them that are supposed to walk the user through the onboarding process.
And its my understanding that they are pulled from the index.js file inside of Steps/ directory similar to how there would be a root reducer file in a reducers folder to export all of them, the steps component in this case like so:
import glamorous from "glamorous";
import ThemedCard from "../../ThemedCard";
import BusinessAddress from "./BusinessAddress";
import CreatePassword from "./CreatePassword";
import GetInvolved from "./GetInvolved";
import Representatives from "./Representatives";
import Topics from "./Topics";
import MemberBenefits from "./MemberBenefits";
export const StepHeader = glamorous.div({
marginBottom: 20,
marginTop: 20,
fontSize: "2rem",
color: "#757575"
});
const OnboardingCompleted = glamorous(ThemedCard)({
textAlign: "center",
boxShadow: "none !important"
});
export default {
CreatePassword,
BusinessAddress,
Completed: OnboardingCompleted,
GetInvolved,
MemberBenefits,
Topics,
Representatives
};
Well, I added mine MemberBenefits and it does not seem to work, its not rendering with its corresponding route. Where could it not be registering this new step or component?
Okay so the magic is not happening inside of Onboarding/OnBoardingWizard/index.js, its happening inside of Wizard/WizardEngine.js:
import React from "react";
import PropTypes from "prop-types";
import objectToArray from "../../../../common/utils/object-to-array";
// TODO: figure out how to use this without making children of wizard engine tied to wizardStep
// eslint-disable-next-line no-unused-vars
class WizardStep {
constructor({ component, color, order, render }, stepComponents) {
if (!component || !render) {
throw new Error("Component or render must be provided.");
}
let componentValue;
if (component) {
componentValue = this.resolveComponent(component, stepComponents);
if (!!componentValue && !React.isValidElement(componentValue)) {
throw new Error(
"wizard step expected component to be a valid react element"
);
}
} else if (render && typeof render === "function") {
throw new Error("wizard step expected render to be a function");
}
this.Component = componentValue;
this.color = color;
this.order = order;
this.render = render;
}
resolveComponent = (component, stepComponents) => {
const componentValue = component;
if (typeof component === "string") {
const componentValue = stepComponents[component];
if (!componentValue) {
throw new Error("component doesnt exist");
}
}
return componentValue;
};
}
export default class WizardEngine extends React.Component {
static propTypes = {
steps: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
initActiveIndex: PropTypes.oneOfType([PropTypes.func, PropTypes.number]),
stepComponents: PropTypes.object
};
constructor(props) {
super(props);
this.state = {
activeIndex: this.resolveInitActiveIndex(props),
steps: this.buildStepsFromConfig(props)
};
}
componentWillReceiveProps(nextProps) {
this.setState({ steps: this.buildStepsFromConfig(nextProps) });
}
resolveInitActiveIndex = props => {
const { initActiveIndex } = props;
let activeIndex = 0;
if (typeof initActiveIndex === "function") {
activeIndex = initActiveIndex(props);
}
if (typeof initActiveIndex === "number") {
activeIndex = initActiveIndex;
}
return activeIndex;
};
buildStepsFromConfig = props => {
const { steps } = props;
let stepArr = steps;
// validate stepList
if (typeof steps === "object" && !Array.isArray(steps)) {
stepArr = objectToArray(steps);
}
if (!Array.isArray(stepArr)) {
throw new Error(
`Unsupported Parameter: Wizard Engine(steps) expected either (object, array); got ${typeof stepArr}`
);
}
return stepArr;
// return stepArr.map(step => new WizardStep(step));
};
setActiveIndex = activeIndex => {
this.setState({ activeIndex });
};
goForward = () => {
this.setState(prevState => ({
activeIndex: prevState.activeIndex + 1
}));
};
goBack = () => {
this.setState(prevState => ({
activeIndex: prevState.activeIndex - 1
}));
};
render() {
const { children } = this.props;
const childProps = {
...this.state,
setActiveIndex: this.setActiveIndex,
goForward: this.goForward,
goBack: this.goBack,
currentStep: this.state.steps[this.state.activeIndex]
};
if (Array.isArray(children)) {
return (
<div>
{children.map((child, i) => {
if (typeof child === "function") {
return child(childProps);
}
childProps.key = `${child.type.name}_${i}`;
return React.cloneElement(child, childProps);
})}
</div>
);
}
if (typeof children === "function") {
return children(childProps);
}
return children;
}
}
I think the first method load the element only when it needed.
The second method load all methods everytime. Why to load Home when you are in /Products?
The path URL is being mapped on the backend utilizing the Entity Framework similar to the setup you can view here in this documentation:
https://dzone.com/articles/aspnet-core-crud-with-reactjs-and-entity-framework
except it is being done in Express.
So it's not using React-Router in the traditional sense where Express allows it to control the whole mapping route paths to components, but instead the path to the onboarding component is being mapped here inside the Express src/app-server/apiConfig.js like so:
"get-involved-onboarding": {
title: "Get Involved",
url: "/account/onboarding/get-involved",
icon: "explore",
component: "GetInvolved",
progress: {
stepType: "GetInvolved",
hasCompleted: true
}
},

Looking to conditionally call a mutation

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)

React with Redux: Child component does not rerender after props have changed (even though they are not shallow equal)

I'm building an app with React Native using Redux for the state management. I will post my code for all the involved components and the reducer down below, but since that is going to be much, let me describe the problem in a few sentences first.
I have an immutable reducer for my objects called 'waitercalls'. I have a screen (HomeScreen) that renders two components. Each component is a <FlatList /> of objects. The objects (waitercalls) are given to them via props by it's parent (HomeScreen). HomeScreen is connected to Redux via React-Redux' connect() and gets the objects ('waitercalls') via a selector created with Re-Select.
The left list's items can be pressed and upon press dispatch an event to the reducer. Here comes the problem. When the left list's item are pressed, the left list correctly updates (calls render()). The right list though does not re-render, even though it gets the same props.
Why does the left list rerender, but the right list not? The reducer is immutable, the selector is too and even the length of the list changes from one to zero which should eliminate the possibility of a shallow equal.
And now for the code:
waitercallsReducer:
import { createSelector } from "reselect";
const initialState = {};
const waitercallsReducer = (state = initialState, action) => {
if (action.payload && action.payload.entities && action.payload.entities.waitercalls) {
return {
...state,
...action.payload.entities.waitercalls
};
} else {
return state;
}
};
export default waitercallsReducer;
export const getAllWaitercallsNormalizedSelector = state => state.waitercalls;
export const getAllWaitercallsSelector = createSelector(
getAllWaitercallsNormalizedSelector,
waitercalls => Object.values(waitercalls)
);
export const getAllActiveWaitercallsSelector = createSelector(
getAllWaitercallsSelector,
waitercalls => waitercalls.filter(waitercall => !waitercall.done)
);
Action creators:
import { setValues } from "../core/core";
// feature name
export const WAITERCALLS = "[Waitercalls]";
// action creators
export const setValues = (values, type) => ({
type: `SET ${type}`,
payload: values,
meta: { feature: type }
});
export const setWaitercalls = waitercalls => setValues(waitercalls, WAITERCALLS);
HomeScreen:
import React, { Component } from "react";
import { View, TouchableOpacity } from "react-native";
import { SafeAreaView } from "react-navigation";
import { connect } from "react-redux";
import { Icon } from "react-native-elements";
import PropTypes from "prop-types";
// ... I've omitted all the imports so that it's shorter
export class HomeScreen extends Component {
// ... I've omitted navigationOptions and propTypes
render() {
const {
checkins,
customChoiceItems,
menuItemPrices,
menuItems,
orders,
pickedRestaurant,
tables,
waitercalls
} = this.props;
console.log("Rendering HomeScreen");
return (
<SafeAreaView style={styles.container}>
<View style={styles.activeOrders}>
<OrdersList
checkins={checkins}
customChoiceItems={customChoiceItems}
menuItemPrices={menuItemPrices}
menuItems={menuItems}
orders={orders}
restaurantSlug={pickedRestaurant.slug}
tables={tables}
waitercalls={waitercalls}
/>
</View>
<View style={styles.tableOvewView}>
<TableOverview
checkins={checkins}
orders={orders}
tables={tables}
waitercalls={waitercalls}
/>
</View>
</SafeAreaView>
);
}
}
const mapStateToProps = state => ({
checkins: getAllCheckinsSelector(state),
customChoiceItems: getAllCustomChoiceItemsNormalizedSelector(state),
menuItemPrices: getAllMenuItemPricesNormalizedSelector(state),
menuItems: getAllMenuItemsNormalizedSelector(state),
orders: getActiveOrdersSelector(state),
pickedRestaurant: getPickedRestaurantSelector(state),
tables: getAllTablesSelector(state),
waitercalls: getAllActiveWaitercallsSelector(state)
});
export default connect(mapStateToProps)(HomeScreen);
OrdersList (as you can see OrdersList also allows presses for orders, which displays the same erroneous behaviour of not having the TableOverView rerender), which is the left list with the clickable <ListItem />s.
import React, { PureComponent } from "react";
import { FlatList, Image, Text } from "react-native";
import { ListItem } from "react-native-elements";
import { connect } from "react-redux";
import PropTypes from "prop-types";
// ... omitted imports
export class OrdersList extends PureComponent {
// omitted propTypes
keyExtractor = item => item.uuid;
registerItem = item => {
// Remember the order status, in case the request fails.
const { restaurantSlug, setOrders } = this.props;
const itemStatus = item.orderStatus;
const data = {
restaurant_slug: restaurantSlug,
order_status: "registered",
order_uuid: item.uuid
};
setOrders({
entities: { orders: { [item.uuid]: { ...item, orderStatus: data.order_status } } }
});
postOrderStatusCreate(data)
.then(() => {})
.catch(err => {
// If the request fails, revert the order status change and display an alert!
alert(err);
setOrders({ entities: { orders: { [item.uuid]: { ...item, orderStatus: itemStatus } } } });
});
};
answerWaitercall = item => {
const { restaurantSlug, setWaitercalls } = this.props;
const data = {
done: true,
restaurant_slug: restaurantSlug
};
setWaitercalls({ entities: { waitercalls: { [item.uuid]: { ...item, done: true } } } });
putUpdateWaitercall(item.uuid, data)
.then(() => {})
.catch(err => {
alert(err);
setWaitercalls({ entities: { waitercalls: { [item.uuid]: { ...item, done: false } } } });
});
};
renderItem = ({ item }) => {
const { checkins, customChoiceItems, menuItemPrices, menuItems, tables } = this.props;
return item.menuItem ? (
<ListItem
title={`${item.amount}x ${menuItems[item.menuItem].name}`}
leftElement={
<Text style={styles.amount}>
{tables.find(table => table.checkins.includes(item.checkin)).tableNumber}
</Text>
}
rightTitle={`${
menuItemPrices[item.menuItemPrice].label
? menuItemPrices[item.menuItemPrice].label
: menuItemPrices[item.menuItemPrice].size
? menuItemPrices[item.menuItemPrice].size.size +
menuItemPrices[item.menuItemPrice].size.unit
: ""
}`}
subtitle={`${
item.customChoiceItems.length > 0
? item.customChoiceItems.reduce((acc, customChoiceItem, index, arr) => {
acc += customChoiceItems[customChoiceItem].name;
acc += index < arr.length - 1 || item.wish ? "\n" : "";
return acc;
}, "")
: null
}${item.wish ? "\n" + item.wish : ""}`}
onPress={() => this.registerItem(item)}
containerStyle={styles.alignTop}
bottomDivider={true}
/>
) : (
<ListItem
title={
item.waitercallType === "bill"
? SCREEN_TEXT_HOME_BILL_CALLED
: SCREEN_TEXT_HOME_SERVICE_ASKED
}
leftElement={
<Text style={styles.amount}>
{
tables.find(table =>
table.checkins.includes(
checkins.find(checkin => checkin.consumer === item.consumer).uuid
)
).tableNumber
}
</Text>
}
rightIcon={{
type: "ionicon",
name: item.waitercallType === "bill" ? "logo-euro" : "ios-help-circle-outline"
}}
onPress={() => this.answerWaitercall(item)}
bottomDivider={true}
/>
);
};
render() {
const { orders, waitercalls } = this.props;
return (
<FlatList
keyExtractor={this.keyExtractor}
data={[...orders, ...waitercalls]}
renderItem={this.renderItem}
// ... omitted ListHeader and ListEmpty properties
/>
);
}
}
export default connect(
null,
{ setOrders, setWaitercalls }
)(OrdersList);
TableOverview, which is the right <FlatList />:
import React, { Component } from "react";
import { FlatList } from "react-native";
import PropTypes from "prop-types";
// ... omitted imports
export class TableOverview extends Component {
// ... omitted propTypes
keyExtractor = item => item.uuid;
renderItem = ({ item }) => {
const { checkins, orders, waitercalls } = this.props;
if (item.invisible) return <Table table={item} />;
console.log("Rendering TableOverview");
return (
<Table
table={item}
hasActiveOrders={orders.some(order => item.userOrders.includes(order.uuid))}
billWanted={item.checkins.some(checkin =>
waitercalls.some(
waitercall =>
waitercall.waitercallType === "bill" &&
waitercall.consumer ===
checkins.find(checkinObj => checkinObj.uuid === checkin).consumer
)
)}
serviceWanted={item.checkins.some(checkin =>
waitercalls.some(
waitercall =>
waitercall.waitercallType === "waiter" &&
waitercall.consumer ===
checkins.find(checkinObj => checkinObj.uuid === checkin).consumer
)
)}
/>
);
};
formatData = (data, numColumns) => {
const numberOfFullRows = Math.floor(data.length / numColumns);
let numberOfElementsLastRow = data.length - numberOfFullRows * numColumns;
while (numberOfElementsLastRow !== numColumns && numberOfElementsLastRow !== 0) {
data.push({ uuid: `blank-${numberOfElementsLastRow}`, invisible: true });
numberOfElementsLastRow++;
}
return data;
};
render() {
const { tables } = this.props;
return (
<FlatList
style={styles.container}
keyExtractor={this.keyExtractor}
data={this.formatData(tables, NUM_COLUMNS)}
renderItem={this.renderItem}
numColumns={NUM_COLUMNS}
/>
);
}
}
export default TableOverview;
I found the solution!
The List was not rerendering, because the <FlatList /> only looked at the tables and not the waitercalls.
I had to add the following property to the <FlatList />:
extraData={[...checkins, ...orders, ...waitercalls]}

Resources