Accessing Apollo's loading boolean outside of Mutation component - reactjs

The Mutation component in react-apollo exposes a handy loading boolean in the render prop function which is ideal for adding loaders to the UI whilst a request is being made. In the example below my Button component calls the createPlan function when clicked which initiates a GraphQL mutation. Whilst this is happening a spinner appears on the button courtesy of the loading prop.
<Mutation mutation={CREATE_PLAN}>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>
The issue I have is that other aspects of my UI also need to change based on this loading boolean. I have tried lifting the Mutation component up the React tree so that I can manually pass the loading prop down to any components which rely on it, which works, but the page I am building has multiple mutations that can take place at any given time (such as deleting a plan, adding a single item in a plan, deleting a single item in a plan etc.) and having all of these Mutation components sitting at the page-level component feels very messy.
Is there a way that I can access the loading property outside of this Mutation component? If not, what is the best way to handle this problem? I have read that you can manually update the Apollo local state using the update function on the Mutation component (see example below) but I haven't been able to work out how to access the loading value here (plus it feels like accessing the loading property of a specific mutation without having to manually write it to the cache yourself would be a common request).
<Mutation
mutation={CREATE_PLAN}
update={cache => {
cache.writeData({
data: {
createPlanLoading: `I DON"T HAVE ACCESS TO THE LOADING BOOLEAN HERE`,
},
});
}}
>
{(createPlan, { loading }) => (
<Button
onClick={() => createPlan({ variables: { input: {} } })}
loading={loading}
>
Save
</Button>
)}
</Mutation>

I face the same problem in my projects and yes, putting all mutations components at the page-level component is very messy. The best way I found to handle this is by creating React states. For instance:
const [createPlanLoading, setCreatePLanLoading] = React.useState(false);
...
<Mutation mutation={CREATE_PLAN} onCompleted={() => setCreatePLanLoading(false)}>
{(createPlan, { loading }) => (
<Button
onClick={() => {
createPlan({ variables: { input: {} } });
setCreatePLanLoading(true);
}
loading={loading}
>
Save
</Button>
)}
</Mutation>

I like the answer with React States. However, when there are many different children it looks messy with so many variables.
I've made a bit update for it for these cases:
const Parent = () => {
const [loadingChilds, setLoading] = useState({});
// check if at least one child item is loading, then show spinner
const loading = Object.values(loadingChilds).reduce((t, value) => t || value, false);
return (
<div>
{loading ? (
<CircularProgress />
) : null}
<Child1 setLoading={setLoading}/>
<Child2 setLoading={setLoading}/>
</div>
);
};
const Child1 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child1 !== loading ? { ...prev, Child1: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 1 Content
</div>
);
};
const Child2 = ({ setLoading }) => {
const [send, { loading }] = useMutation(MUTATION_NAME2);
useEffect(() => {
// add info about state to the state object if it's changed
setLoading((prev) => (prev.Child2 !== loading ? { ...prev, Child2: loading } : prev));
});
const someActionHandler = (variables) => {
send({ variables});
};
return (
<div>
Child 2 Content
</div>
);
};

Related

ReactJS - Alternative to passing useState between files

Background
I have a file that presents my main page Dash.js
This presents some data from an API on a "card", from two other files List.js and ListLoading.js
I have an additional "card" which I can trigger open with default useState value of 1, and the onClick works to close, as you will see in the dash.js file.
Current Code
//Dash.js
function Dash(props) {
//control additional card
const [openCard, setopenCard] = React.useState(0);
const closeCard = () => {
setopenCard(0);
}
//set API repo
const apiUrl = (`http://example.com/api/`);
axios.get(apiUrl, {
withCredentials: true }).then((res) =>{
setAppState({ loading: false, repos: res.data.emails });
});
return (
{(openCard>0 &&
<Card>
<Cardheader onClick={() => closeCard()}>
Click here to close
</Cardheader>
<Cardbody>
Some data here
</Cardbody>
</Card>
)
|| null
}
<Card>
<ListLoading isLoading={appState.loading} repost={appState.repos} />
<Card>
);
}
//List.js
const List = (props) => {
const { repos } = props;
if (!repos || repos.length === 0) return <p>No data available</p>;
for (var key in repos) {
return (
{repos.map((repo) => {
return (
<p className='repo-text max-width' >ID:{repo.id}{" "}Value:{repo.value} </p>
);}
)}
);}
};
export default List;
//ListLoading.js
function WithListLoading(Component) {
return function WihLoadingComponent({ isLoading, ...props }) {
if (!isLoading) return <Component {...props} />;
return (
<p style={{ textAlign: 'center', fontSize: '30px' }}>
Fetching data may take some time, please wait
</p>
);
};
}
export default WithListLoading;
Desired Outcome
I want to set the the value for openCard.useState() to the repos.id.
e.g. onClick={() => openCard({repos.id})}
The complication of this is that I need to retrieve that code from List.js and pass it to the useState for the openCard, which is in Dash.js.
I am still fairly new to react so this is proving a little tricky to work out how to do.
What I've tried
I have looked into useContext, but either it has confused me or I am right to think this would not work for what I am trying to do.
I have looked into redux, however this seems like that may be overkill for this solution.
I have tried a series of passing the different constants via import/export however I now understand that useState is not designed to work this way and should really be used within the function/class where it is contained.
So any thoughts to remedy would be greatly appreciated!
So, just to restate what I understood your issue to be:
You have a parent component that renders a list of objects and can render a detail card of one of the object.
You want to have a single item in your list of objects be able to tell the parent "please open card 123".
Now to look at the options you considered:
Redux I agree Redux is overkill for this. Redux is usually only necessary if you need complex, possibly async reading and writing to a single shared datasource across the whole scope of your application. For a little UI interaction like this, it is definitely not worth setting up Redux.
React Context Context relies on a Provider component, which you wrap some chunk of your app in. Any component below that Provider can then use useContext to reach into the memory of that Provider. You can store anything in there that you could store in a component, from a single state variable up to a more complex useReducer setup. So, in a way, this basically does what you were hoping to do with static variables passing the state around. This is the right solution if you were going to be using this state value across a wide variety of components.
Props are probably the right way to go here - since you have a parent who wants to get messages from a child directly you can give the child a callback function. This is the same as the onClick function you can give a button, except here you can pass your list a onShowCard function.
In your Dash:
<ListLoading
isLoading={appState.loading} repost={appState.repos}
onShowCard={(cardId) => setopenCard(cardId)} />
At the end of the List:
{repos.map((repo) => {
return (
<button key={repo.id} className='repo-text max-width' onClick={() => { props.onShowCard(repo.id) }>
ID:{repo.id}{" "}Value:{repo.value}
</button>
);}
)}
You can pass on the function to update state to ListLoading component which will be forwarded to List component assuming it is wrapped by thee HOC WithListLoading.
Inside List you can then attach and onClick on the element to pass on the id of the clicked element
function Dash(props) {
//control additional card
const [openCard, setopenCard] = React.useState(0);
const closeCard = () => {
setopenCard(0);
}
//set API repo
const apiUrl = (`http://example.com/api/`);
axios.get(apiUrl, {
withCredentials: true
}).then((res) =>{
setAppState({ loading: false, repos: res.data.emails });
});
const handleOpen = id => {
setopenCard(id);
}
return (
{(openCard>0 &&
<Card>
<Cardheader onClick={() => closeCard()}>
Click here to close
</Cardheader>
<Cardbody>
Some data here
</Cardbody>
</Card>
)
|| null
}
<Card>
<ListLoading isLoading={appState.loading} repost={appState.repos} handleOpen={handleOpen} />
<Card>
);
}
const List = (props) => {
const { repos, handleOpen } = props;
if (!repos || repos.length === 0) return <p>No data available</p>;
for (var key in repos) {
return (
{repos.map((repo) => {
return (
<p className='repo-text max-width' onClick={() => props.handleOpen(repo.id)} >ID:{repo.id}{" "}Value:{repo.value} </p>
);}
)}
);}
};
export default List;

prevent child component to re-render below context provider with memo

I am using a context provider in React to share data across several components. However since a value gets changed from one of my subcomponents, it rerenders all of my other components which partly leads to performance issues. So I want to prevent my child components to rerender. I tried using React.memo() but it's still rendering whenever I set the state of the Context Provider.
const Authenticator = React.memo(() => {
const [myChat, setMyChat] = useContext(ChatContext);
console.log("rerender"); // gets called everytime on click
return (
<Button
title="click me"
onPress={() => setMyChat({ text: "hello" })}
></Button>
);
});
My Context Provider looks like this:
const ChatProvider = ({ children }) => {
const [myChat, setMyChat] = useState([]);
return (
<ChatContext.Provider value={[myChat, setMyChat]}>
{children}
</ChatContext.Provider>
);
};
My App.js looks like this:
<ChatProvider>
<Authenticator />
</ChatProvider>
React.Memo doesn't help since you are calling the useContext hook which will cause the component to re-render every time the value from the provider changes. You should consider splitting your context into two separate contexts: one for the value, one for the state updater.
const ChatProvider = ({ children }) => {
const [myChat, setMyChat] = useState([])
return (
<ChatDispatchContext.Provider value={setMyChat}>
<ChatValueContext.Provider value={myChat}>
{children}
</ChatValueContext.Provider>
</ChatDispatchContext.Provider>
)
}
Then, update your Authenticator component to the following:
const Authenticator = React.memo(() => {
const setMyChat = useContext(ChatDispatchContext)
return (
<Button
title="click me"
onPress={() => setMyChat({ text: "hello" })}
></Button>
)
})

React PDFDownloadLink - call onclick event through code explicitly

PDFDownloadLink from react-pdf library downloads a pdf when someone clicks on it.
I want to trigger this click event via code based on some condition .
How do I explicitly invoke the click of PDFDownloadLink through code?
A bit late, but you can pass a ref to the render function's return value and use it to call click() on imperatively. For that to work you need to use a separate component wrapper:
const DownloadLink = memo(function () {
const linkRef = useRef(null)
const onLoadingFinished = useCallback(function () {
// When this function is called the first time it is safe to initiate the download
const elem = linkRef?.current
if (elem !== null) {
elem.click()
}
}, [])
return (
<PDFDownloadLink document={<MyDoc />} fileName={'my-file.pdf'}>
{({ blob, url, loading, error }) => (
// You shouldn't call setState() here, so we need to use a separate component to keep track of whether the document has finished rendering
<WorkaroundContainer ref={linkRef} loading={loading} onLoadingFinished={onLoadingFinished} />
)}
</PDFDownloadLink>
)
})
const WorkaroundContainer = forwardRef(function ({ loading, onLoadingFinished }, ref) {
useEffect(() => {
if (!loading) {
onLoadingFinished()
}
}, [loading])
// If you only want to initiate the download imperatively, hide the element via CSS (e.g. `visibility: hidden`)
return (
<div ref={ref}>
{loading ? 'Loading...' : 'Download PDF'}
</div>
)
})

How to render a different component with React Hooks

I have a parent component with an if statement to show 2 different types of buttons.
What I do, on page load, I check if the API returns an array called lectures as empty or with any values:
lectures.length > 0 ? show button A : show button B
This is the component, called main.js, where the if statement is:
lectures.length > 0
? <div onClick={() => handleCollapseClick()}>
<SectionCollapse open={open} />
</div>
: <LectureAdd dataSection={dataSection} />
The component LectureAdd displays a + sign, which will open a modal to create a new Lecture's title, while, SectionCollapse will show an arrow to show/hide a list of items.
The logic is simple:
1. On page load, if the lectures.lenght > 0 is false, we show the + sign to add a new lecture
OR
2. If the lectures.lenght > 0 is true, we change and show the collpase arrow.
Now, my issue happens when I add the new lecture from the child component LectureAdd.js
import React from 'react';
import { Form, Field } from 'react-final-form';
// Constants
import { URLS } from '../../../../constants';
// Helpers & Utils
import api from '../../../../helpers/API';
// Material UI Icons
import AddBoxIcon from '#material-ui/icons/AddBox';
export default ({ s }) => {
const [open, setOpen] = React.useState(false);
const [ lucturesData, setLecturesData ] = React.useState(0);
const { t } = useTranslation();
const handleAddLecture = ({ lecture_title }) => {
const data = {
"lecture": {
"title": lecture_title
}
}
return api
.post(URLS.NEW_COURSE_LECTURE(s.id), data)
.then(data => {
if(data.status === 201) {
setLecturesData(lucturesData + 1) <=== this doesn't trigger the parent and the button remains a `+` symbol, instead of changing because now `lectures.length` is 1
}
})
.catch(response => {
console.log(response)
});
}
return (
<>
<Button variant="outlined" color="primary" onClick={handleClickOpen}>
<AddBoxIcon />
</Button>
<Form
onSubmit={event => handleAddLecture(event)}
>
{
({
handleSubmit
}) => (
<form onSubmit={handleSubmit}>
<Field
name='lecture_title'
>
{({ input, meta }) => (
<div className={meta.active ? 'active' : ''}>
<input {...input}
type='text'
className="signup-field-input"
/>
</div>
)}
</Field>
<Button
variant="contained"
color="primary"
type="submit"
>
ADD LECTURE
</Button>
</form>
)}
</Form>
</>
)
}
I've been trying to use UseEffect to trigger a re-render on the update of the variable called lucturesData, but it doesn't re-render the parent component.
Any idea?
Thanks Joe
Common problem in React. Sending data top-down is easy, we just pass props. Passing information back up from children components, not as easy. Couple of solutions.
Use a callback (Observer pattern)
Parent passes a prop to the child that is a function. Child invokes the function when something meaningful happens. Parent can then do something when the function gets called like force a re-render.
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((lecture) => {
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Call API
let lecture = callApi();
// Notify parent of event
onLectureCreated(lecture);
}, [onLectureCreated]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Similar to solution #1, except for Parent handles API call. The benefit of this, is the Child component becomes more reusable since its "dumbed down".
function Parent(props) {
const [lectures, setLectures] = useState([]);
const handleLectureCreated = useCallback((data) => {
// Call API
let lecture = callApi(data);
// Force a re-render by calling setState
setLectures([...lectures, lecture]);
}, []);
return (
<Child onLectureCreated={handleLectureCreated} />
)
}
function Child({ onLectureCreated }) {
const handleClick = useCallback(() => {
// Create lecture data to send to callback
let lecture = {
formData1: '',
formData2: ''
}
// Notify parent of event
onCreateLecture(lecture);
}, [onCreateLecture]);
return (
<button onClick={handleClick}>Create Lecture</button>
)
}
Use a central state management tool like Redux. This solution allows any component to "listen in" on changes to data, like new Lectures. I won't provide an example here because it's quite in depth.
Essentially all of these solutions involve the same solution executed slightly differently. The first, uses a smart child that notifies its parent of events once their complete. The second, uses dumb children to gather data and notify the parent to take action on said data. The third, uses a centralized state management system.

Refactoring class component to functional component with hooks, getting Uncaught TypeError: func.apply is not a function

This is my first attempt to refactor code from a class component to a functional component using React hooks. The reason we're refactoring is that the component currently uses the soon-to-be-defunct componentWillReceiveProps lifecylcle method, and we haven't been able to make the other lifecycle methods work the way we want. For background, the original component had the aforementioned cWRP lifecycle method, a handleChange function, was using connect and mapStateToProps, and is linking to a repository of tableau dashboards via the tableau API. I am also breaking the component, which had four distinct features, into their own components. The code I'm having issues with is this:
const Parameter = (props) => {
let viz = useSelector(state => state.fetchDashboard);
const parameterSelect = useSelector(state => state.fetchParameter)
const parameterCurrent = useSelector(state => state.currentParameter)
const dispatch = useDispatch();
let parameterSelections = parameterCurrent;
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [props.parameterCurrent])
const handleParameterChange = (event, valKey, index, key) => {
parameterCurrent[key] = event.target.value;
console.log(parameterCurrent[key]);
return (
prevState => ({
...prevState,
parameterSelections: parameterCurrent
}),
() => {
viz
.getWorkbook()
.changeParameterValueAsync(key, valKey)
.then(function () {
Swal.fire({
position: "center",
icon: "success",
title:
JSON.stringify(key) + " set to " + JSON.stringify(valKey),
font: "1em",
showConfirmButton: false,
timer: 2500,
heightAuto: false,
height: "20px"
});
})
.otherwise(function (err) {
alert(
Swal.fire({
position: "top-end",
icon: "error",
title: err,
showConfirmButton: false,
timer: 1500,
width: "16rem",
height: "5rem"
})
);
});
}
);
};
const classes = useStyles();
return (
<div>
{Object.keys(parameterSelect).map((key, index) => {
return (
<div>
<FormControl component="fieldset">
<FormLabel className={classes.label} component="legend">
{key}
</FormLabel>
{parameterSelect[key].map((valKey, valIndex) => {
console.log(parameterSelections[key])
return (
<RadioGroup
aria-label="parameter"
name="parameter"
value={parameterSelections[key]}
onChange={(e) => dispatch(
handleParameterChange(e, valKey, index, key)
)}
>
<FormControlLabel
className={classes.formControlparams}
value={valKey}
control={
<Radio
icon={
<RadioButtonUncheckedIcon fontSize="small" />
}
className={clsx(
classes.icon,
classes.checkedIcon
)}
/>
}
label={valKey}
/>
</RadioGroup>
);
})}
</FormControl>
<Divider className={classes.divider} />
</div>
);
})
}
</div >
)};
export default Parameter;
The classes const is defined separately, and all imports of reducers, etc. have been completed. parameterSelect in the code points to all available parameters, while parameterCurrent points to the default parameters chosen in the dashboard (i.e. what the viz initially loads with).
Two things are happening: 1. Everything loads fine on initial vizualization, and when I click on the Radio Button to change the parameter, I can see it update on the dashboard - however, it's not actually showing the radio button as being selected (it still shows whichever parameter the viz initialized with as being selected). 2. When I click outside of the Filterbar (where this component is imported to), I get Uncaught TypeError: func.apply is not a function. I refactored another component and didn't have this issue, and I can't seem to determine if I coded incorrectly in the useEffect hook, the handleParameterChange function, or somewhere in the return statement. Any help is greatly appreciated by this newbie!!!
This is a lot of code to take in without seeing the original class or having a code sandbox to load up. My initial thought is it might be your useEffect
In your refactored code, you tell your useEffect to only re-run when the props.parameterCurrent changes. However inside the useEffect you don't make use of props.parameterCurrent, you instead make use of parameterCurrent from the local lexical scope. General rule of thumb, any values used in the calculations inside a useEffect should be in the list of re-run dependencies.
useEffect(() => {
let keys1 = Object.keys(parameterCurrent);
if (
keys1.length > 0 //if parameters are available for a dashboard
) {
return ({
parameterSelections: parameterCurrent
});
}
}, [parameterCurrent])
However, this useEffect doesn't seem to do anything, so while its dependency list is incorrect, I don't think it'll solve the problem you are describing.
I would look at your dispatch and selector. Double check that the redux store is being updated as expected, and that the new value is making it from the change callback, to the store, and back down without being lost due to improper nesting, bad key names, etc...
I'd recommend posting a CodeSandbox.io link or the original class for further help debugging.

Resources