in a WordPress Gutenberg plugin
hooks_addFilter_editor_blockEdit = (BlockEdit) => {
return (props) => {
apiFetch({url: 'https://example.com/api/'+props.attributes.content
}).then(_return => {
props.attributes.content = _return;
// What should I use here to force re-render the block?
})
return ( <BlockEdit { ...props } /> );
}
}
wp.hooks.addFilter( 'editor.BlockEdit', 'my-plugin-slug', hooks_addFilter_editor_blockEdit );
For the above code, my plugin sends the props.attributes.content into an external API, and updates it asynchronously. Visually, as many default gutenberg blocks use props.attributes.content to display the content of the block (paragraph blocks for instance), that content gets updated on the editor, automatically, but not immediately. Re-rendering of the block happens only when my cursor gets off the block, or when I get the input focus out of this block.
What can I add to the above code to force the editor to visually show the updated content as soon as the apiFetch call has succeeded?
Try if this works for you:
hooks_addFilter_editor_blockEdit = (BlockEdit) => {
return (props) => {
// Destructure "props"
const { attributes, setAttributes } = props;
apiFetch({url: 'https://example.com/api/'+attributes.content
}).then(_return => {
// What should I use here to force re-render the block?
setAttributes( { content: _return } );
})
return ( <BlockEdit { ...props } /> );
}
}
wp.hooks.addFilter( 'editor.BlockEdit', 'my-plugin-slug', hooks_addFilter_editor_blockEdit );
Still, while typing in a block, there might be a slight delay until your api responds. Also maybe you need to wrap your BlockEdit in a HigherOrderComponent.
Related
I'm using React functional components and here are my codes:
Parent component function:
const calculateAPR = async (val) => {
setIsAprLoading(true);
try {
if (val.addr !== "" && val.date !== null) {
const totalStaking = await someEP.getSomeData(val.addr);
val.staked = totalStaking;
setResData((prevState) => {
return ({
...prevState,
aprRes: val
})
})
setRenderApr(true);
setIsAprLoading(false);
}
else {
setRenderApr(false);
alert(Constants.ADDR_N_DATE_ERR);
}
}
catch (err) {
setRenderApr(false);
console.log(err);
alert(Constants.ADDR_NO_DATA);
}
finally {
setIsAprLoading(false);
}
}
...
return (
...
<QueryAprField text={Constants.CALC_APR_HEADER} onFunction={calculateAPR} isLoading={isAprLoading} />
<CalculateAprField resData={resData.aprRes} onRender={renderApr} />
...
)
Child component 1:
function QueryAprField(props) {
...
const handleQuery = () => {
const verify = verifyDelegatorAddress();
if (verify) {
props.onFunction(queryValue);
}
else {
alert(Constants.ENTER_VALID_DEL_ADDR);
}
}
...handles taking in user inputs and passing it to parent component...
}
Child component 2:
function CalculateAprField(props) {
const aprRes = props.resData;
...
const renderCard = () => {
if (renderData == true) {
const aprInput = setAprInputs(aprRes);
const { staked } = extractAprInput(aprInput);
const apr = parseFloat(calculateAPR(staked, accrued, withdrawn, numOfDays).toFixed(5));
if (isNaN(apr)) {
//How to reset aprRes and ensure that its not using old values
return alert(Constants.APR_AUTO_ERR)
}
return (
<Paper elevation={4}>
...some html and css...
</Paper>
)
}
}
return (
<Box>
{renderCard()}
</Box>
)
I'm trying to enable a situation where, after calculateAPR in the parent component is executed, some data will be passed to child component 2; and in child component 2 in the renderCard function, if the variable apr in child component 2 is NaN then an alert will be triggered. However, the problem I'm facing now is that after the alert is triggered, and when I put in new values and execute calculateAPR again, child component 2 seems to use the old values first before using the new values that are passed down from the parent component. So in other words, I get the alert first and then it uses the new values that are being passed down.
How can I enable the aprRes variable in child component 2, to reset its value after the alert is thrown? So that the alert is not thrown twice?
There is no need to reset the value. component rerenders only on props change.
I see the problem could be with this line. const aprInput = setAprInputs(aprRes); React setState doesn't return anything. Please change it to below and try once.
setAprInputs(aprRes);
(or)
if aprInput is not used anywhere else better extract from aprRes only.
const { staked } = extractAprInput(aprRes);
Incase if setAprInputs is not a setState rather a user-defined function, ensure it is a synchronous function and console.log after the call.
hope this gives you some insight to debug.
I'm using Antd Input library, whenever I type in the start or in the middle of the word my cursor jumps to the end.
const handleOpenAnswer =( key, value )=>{
handleFieldChange({
settings: {
...settings,
[key]: value
}
})
}
return (
<Input
required
size='default'
placeholder='Label for Diference Open Answer Question'
value='value'
onChange={({ target: { value } }) => {
handleOpenAnswer('differenceOpenAnswerLabel', value)
}}
/>
The reason why your cursor always jumps to the end is because your parent component gets a new state and therefore re-renders its child components. So after every change you get a very new Input component. So you could either handle the value change within the component itself and then try to pass the changed value up to the parent component after the change OR (and I would really recommend that) you use something like React Hook Form or Formik to handle your forms. Dealing with forms on your own can be (especially for complex and nested forms) very hard and ends in render issues like you face now.
Example in React-Hook-Form:
import { FormProvider, useFormContext } = 'react-hook-form';
const Form = () => {
const methods = useForm();
const { getValues } = methods;
const onSubmit = async () => {
// whatever happens on submit
console.log(getValues()); // will print your collected values without the pain
}
return (
<FormProvider {...methods}>
<form onSubmit={(e) => handleSubmit(onSubmit)(e)>
{/* any components that you want */}
</form>
</FormProvider>
);
}
const YourChildComponent = () => {
const { register } = useFormContext();
return (
<Input
{...register(`settings[${yourSettingsField}]`)}
size='default'
placeholder='Label for Diference Open Answer Question'
/>
)
}
I have a simple React component that injects an instance of the Rich Text Editor, TinyMCE into any page.
It is working, but sometimes a bad prop value gets through and causes errors.
I was wondering, if there is a way to check if the values of planetId or planetDescriptor are either empty or null before anything else on the page loads.
I tried wrapping all the code in this:
if(props)
{
const App = (props) => { ... }
}
But that always throws this error:
ReferenceError: props is not defined
Is there a way to check for certain values in props before I finish loading the component?
thanks!
Here is the app:
const App = (props) => {
const [planetDescriptor, setPlanetDescriptorState] = useState(props.planetDescriptor || "Planet Descriptor...");
const [planetId, setPlanetIdState] = useState(props.planetId);
const [planet, setPlanetState] = useState(props.planet);
const [dataEditor, setDataEditor] = useState();
const handleEditorChange = (data, editor) => {
setDataEditor(data);
}
const updatePlanetDescriptor = (data) => {
const request = axios.put(`/Planet/${planetId}/planetDescriptor`);
}
return (
<Editor
id={planetId.toString()}
initialValue={planetDescriptor}
init={{
selector: ".planetDescriptor",
menubar: 'edit table help'
}}
value={dataEditor}
onEditorChange={handleEditorChange}
/>
)
}
export default App;
You had the right idea in the conditional. Just need to put it inside the component rather than wrapping the whole thing. What you can try is something similar to what the react docs for conditional rendering has for a sample. What this does is it check if the props = null / undefined and then returns or renders the error state. Else it returns the Editor.
if (!props) {
return <h1>error state</h1>
}
return <Editor></Editor>
You can't wrap the code in the way you tried as you are working with JSX, not plain javascript, so you can't use the if statement there.
I suggest using a ternary, like so:
const SomeParentComponent = () => {
const propsToPass = dataFetchOrWhatever;
return (
<>
{propsToPass.planetDescriptor && propsToPass.planetId ?
<App
planetDescriptor={propsToPass.planetDescriptor}
planetId={propsToPass.planetId}
anyOtherProps={???}
/> :
null
}
</>
)
};
This will conditionally render the App component, only if both of those props exist.
You can also use && to have the same effect:
... code omitted ...
{propsToPass.planetDescriptor && propsToPass.planetId &&
<App
planetDescriptor={propsToPass.planetDescriptor}
planetId={propsToPass.planetId}
anyOtherProps={???}
/>
}
... code omitted ...
Which approach you use is largely up to preference and codebase consistency.
I've got really weird problem. I mean, to me, really. This is not some kind of "use setState instead of this.state" problem. I'm working on trip planning App. I'm using ContextAPI which provides login information and user data (logged user trips etc ) to the whole app.
"Schedule" component, showing day-by-day trip schedule is context subscriber.
Now I want to delete one of the trip days (Day, also subscribing to the context is component rendered by Schedule Component on the Schedule list). And then magic happens:
when I do it right after logging in to the app, everything is working fine. But when I'll refresh the page and delete another day, context state (which as I said, holds all the data, including Days on schedule list) changes (checked via developer tools) but Schedule component is not re-rendering itself, so deleted element remains visible.
But as I said, state changes, and everything is working fine without page refresh before deleting operation. Also when I switch to another tab (Schedule is just one of the tabs, I've got for example Costs and Notes tab on the navbar rendering Costs and Notes components), and then go back to the Schedule tab it re-renders and everything is looking fine, component is showing info accordingly to context state.
Code works like this: deletion (context function ) is triggered by delete icon click in Day.js (context subscriber), Day.js is rendered by Schedule.js (context subscriber too).
In Context.js
... context functions ...
//WRAPPER FOR DELETE FUNCTIONS
delete(url, deleteFunc) {
this.requestsApi.request(url, "DELETE", null)
.then(response => {
if(response.status===204) {
deleteFunc();
} else {
this.toggleRequestError(true);
}
})
.catch( err => {
console.log(err);
this.toggleRequestError(true);
});
}
deleteDay = (id, tripId) => {
let newTrip = this.state.userData.trips.find((trip => trip.id ===
tripId));
newTrip.days = newTrip.days.filter(day => day.id !== id)
this.updateTrip(newTrip);
}
updateTrip = (newTrip) => {
let trips = this.state.userData.trips;
this.setState(prevState => ({
userData : {
...prevState.userData,
trips: trips.map(trip => {
if(trip.id !== newTrip.id)
return trip;
else
return newTrip;
})
}
}
));
}
...
In Schedule.js
... in render's return ...
<ul>
{this.trip.days.map((day,index) => {
return <DayWithContext
inOverview={false}
key={index}
number={index}
day={day}
tripId={this.trip.id}
/>
})}
</ul>
....
In Day.js (delete icon)
...
<img
src={Delete}
alt="Delete icon"
className="mr-2"
style={this.icon}
onClick={() => {
this.props.context.delete(
`/days/${this.props.day.id}`,
() => this.props.context.deleteDay(this.props.day.id,
this.props.tripId)
);
}}
/>
...
Anyone is having any idea what's going on and why Schedule can be not re-rendered after deletion done after page refresh? And even if context state changes? Only idea I have for now is that this is some kind of a bug...
//UPDATE
In Schedule component, in function componentWillReceiveProps() I console log props.context.trip[0].day[0] - first day. It's not an object, just plain text, so its not evaluated "as I click on it in the console".
Before I delete it, console logs first item. After I delete it, console logs second item (so now it's first item on the list) so props given to Schedule are changing, but no render is triggered... What the hell is going on there?
//UPDATE 2
I've also noticed that when I switch to Schedule component from another component, it works good (for example I'm refreshing the page on /costs endpoint then click Schedule tab on navbar menu which leads to /schedule). But when I refresh the page when on /schedule endpoint, this "bug" occurs and component is not re-rendering.
//Render from Context:
...
render() {
const {
isLoggedIn,
wasLoginChecked,
userData,
isLogoutSuccesfulActive,
isLogoutUnSuccesfulActive,
isDataErrorActive,
isRequestErrorActive,
isDataLoaded
} = this.state;
const context = {
isLoggedIn,
wasLoginChecked,
isLogoutSuccesfulActive,
isLogoutUnSuccesfulActive,
isDataErrorActive,
isRequestErrorActive,
userData,
isDataLoaded,
requestsApi : this.requestsApi,
toggleLogin : this.toggleLogin,
checkLogin: this.checkLogin,
setUserData: this.setUserData,
loadData: this.loadData,
addTrip: this.addTrip,
delete: this.delete,
deleteDay: this.deleteDay,
deleteActivity: this.deleteActivity,
deleteTrip: this.deleteTrip,
updateTrip: this.updateTrip,
uncheckLogin: this.uncheckLogin,
toggleLogoutSuccesful: this.toggleLogoutSuccesful,
toggleLogoutUnSuccesful: this.toggleLogoutUnSuccesful,
toggleRequestError: this.toggleRequestError,
toggleDataError: this.toggleDataError
};
return (
<Context.Provider value={context}>
{this.props.children}
</Context.Provider>
)
}
}
export const Consumer = Context.Consumer;
-------------Context HOC:
export default function withContext(Component) {
return function ContextComponent(props) {
return (
<Context.Consumer>
{context => <Component {...props} context={context} />}
</Context.Consumer>
);
}
}
You mutate your state.
In this place, you change an object in the state without using setState:
newTrip.days = newTrip.days.filter(day => day.id !== id)
And then in your map function, you always return the same objects (it is already updated).
So, to fix your issue you can try to change your deleteDay method:
deleteDay = (id, tripId) => {
const trip = this.state.userData.trips.find((trip => trip.id ===
tripId));
const newTrip = {...trip, days: trip.days.filter(day => day.id !== id)};
this.updateTrip(newTrip);
}
UPDATE:
You have one more issue in your Schedule component.
You need to get current trip dynamically, don't set it in the constructor:
Try this:
constructor(props) {
super(props);
this.getTrip = this.getTrip.bind(this);
}
getTrip() {
return this.props.context.userData.trips.find(trip => {
return `${trip.id}`===this.props.match.params.id;
});
}
render() {
const trip = this.getTrip();
return (
...
<ul>
{trip.days.map((day,index) => {
return <DayWithContext
inOverview={false}
key={day.id}
number={index}
day={day}
tripId={trip.id}
/>
})}
</ul>
...
)
}
I am using Draft.js plugin Linkify.
I am trying to load the content from the local storage and then linkify it.
Now I have to use setTimeout to wait linkifyPlugin ready. If not, the content loaded will be pure text which is not linkified.
Is there any way like event I can use to know when plugin is ready?
class MyComponent extends Component {
constructor(props) {
super(props);
this.state = {
editorState: EditorState.createEmpty()
};
}
componentDidMount() {
// ...
if (hasDraft) this.loadEditor(draftFromLocalStorage);
}
loadEditor = rawContent => {
// here I have to use setTimeout to wait linkifyPlugin ready
setTimeout(() => {
const editorState = EditorState.push(this.state.editorState, convertFromRaw(rawContent));
this.setState({ editorState });
}, 5);
};
render() {
return (
<Editor
editorState={editorState}
plugins={[linkifyPlugin]}
onChange={this.onEditorChange} />
);
}
}
Try using a Promise
loadEditor(rawContent){
return new Promise((resolve, reject) => {
let editorState = EditorState.push(this.state.editorState, convertFromRaw(rawContent));
resolve(editorState);
});
}
//call it after component has mounted
componentDidMount(){
this.loadEditor(draftFromLocalStorage).then(data => this.setState({ editorState: data }));
}
Strange, looking at the source code there's no reason to think the plugin has async code: https://github.com/draft-js-plugins/draft-js-plugins/tree/master/draft-js-linkify-plugin.
You could try putting a breakpoint on this function: https://github.com/draft-js-plugins/draft-js-plugins/blob/master/draft-js-linkify-plugin/src/linkStrategy.js
That function is called for every block in the state, so you should be able to see whether the text is processed by Linkify or not. If you remove the timeout and the function is still being called, then the problem should be somewhere else (because it would mean that you have a problem with the rendering, and not with the actual processing).