Wait for data fetch and then render the block - reactjs

I'm developing a Gutenberg Block for Wordpress and I'm getting stuck with the logic.
I need to fetch some data to populate a ComboboxControl to allow the user to select one option.
So far I could make it work but when the post is saved and the page is reloaded, the saved value of the Combobox cannot match any item from the list because it is loaded afterwards and the selected value appears blank even if it's there. If I click inside the combobox and then outside, the selected value finally shows up.
I had to put the apiFetch request outside of the Edit function to prevent endless calls to the API but I'm not sure this is good practice.
So from there I'm not sure how to improve my code or how to use hooks such as useEffect.
I need to have my data fetched and, only then, render my ComboboxControl with all the options ready.
Here is my code from the edit.js file:
import { __ } from '#wordpress/i18n';
import { useBlockProps } from '#wordpress/block-editor';
import apiFetch from '#wordpress/api-fetch';
import { ComboboxControl, SelectControl } from '#wordpress/components';
import { useState, useEffect } from '#wordpress/element';
import './editor.scss';
var options = [];
apiFetch( { path: '/wp/v2/posts/?per_page=-1' } ).then( ( posts ) => {
console.log( posts );
if ( posts.length ) {
posts.forEach( ( post ) => {
options.push( { value: post.id, label: post.title.rendered } );
} );
}
}, options );
export default function Edit( { attributes, setAttributes } ) {
const blockProps = useBlockProps();
const [filteredOptions, setFilteredOptions] = useState( options );
const updateGroupId = ( val ) => {
setAttributes( { GroupId: parseInt( val ) } );
}
return (
<div {...blockProps}>
<ComboboxControl
label="Group"
value={attributes.GroupId}
onChange={updateGroupId}
options={filteredOptions}
onFilterValueChange={( inputValue ) =>
setFilteredOptions(
options.filter( ( option ) =>
option.label
.toLowerCase()
.indexOf( inputValue.toLowerCase() ) >= 0
)
)
}
/>
</div>
);
}

I have the same problem...
adding my code here too as it is slightly different and might help finding the solution
const { registerBlockType } = wp.blocks;
const { ComboboxControl } = wp.components;
import { useState, useEffect } from '#wordpress/element';
import apiFetch from '#wordpress/api-fetch';
registerBlockType('hm/cptSelect', {
title: 'cptSelect',
category: 'common',
icon: 'smiley',
attributes: {
post_id: {
type: 'number',
default: 0,
},
},
edit: props => {
const { attributes, setAttributes } = props;
//states
const [posts, setPosts] = useState([]);
const [filteredOptions, setFilteredOptions] = useState(posts);
//funcs
const apirequiest = async () => {
const res = await apiFetch({ path: '/wp/v2/posts' });
const options = await res.map(post=> {
return { value: post.id, label: post.title.rendered };
});
setPosts(options);
return;
};
//effects
useEffect(() => {
apirequiest();
}, []);
return (
<>
<ComboboxControl
onFilterValueChange={inputValue =>
setFilteredOptions(
posts.filter(option =>
option.label.toLowerCase().includes(inputValue.toLowerCase())
)
)
}
label='PostSelect'
value={attributes.post}
onChange={value => setAttributes({ post_id: value })}
options={filteredOptions}
/>
</>
);
},
save: props => {
const { attributes } = props;
// apiFetch({ path: `/wp/v2/posts${attributes.post_id}` }).then(res => {
// setPostsData(res[0]);
// });
return (
<>
<div>{attributes.post_id}</div>
</>
);
},
});

Related

How to avoid state reset while using Intersection observer in React.js?

I'm trying to implement intersection observer in react functional component.
import React, { useEffect, useRef, useState } from "react";
import { getData } from "./InfiniteClient";
export default function InfiniteScroll() {
const [data, setData] = useState([]);
const [pageCount, setPageCount] = useState(1);
const sentinal = useRef();
useEffect(() => {
const observer = new IntersectionObserver(intersectionCallback);
observer.observe(sentinal.current, { threshold: 1 });
getData(setData, data, pageCount, setPageCount);
}, []);
const intersectionCallback = (entries) => {
if (entries[0].isIntersecting) {
setPageCount((pageCount) => pageCount + 1);
getData(setData, data, pageCount, setPageCount);
}
};
return (
<section>
{data &&
data.map((photos, index) => {
return <img alt="" src={photos.url} key={index} />;
})}
<div className="sentinal" ref={sentinal}>
Hello
</div>
</section>
);
}
When I'm consoling prevCount above or prevData in the below function is coming as 1 and [] which is the default state.
function getData(setData, prevData, pageCount, setPageCount) {
fetch(
`https://jsonplaceholder.typicode.com/photos?_page=${pageCount}&limit=10`
)
.then((val) => val.json())
.then((val) => {
console.log("prevD", prevData,val,pageCount);
if (!prevData.length) setData([...val]);
else {
console.log("Here", pageCount, prevData, "ddd", val);
setData([...prevData, ...val]);
}
}).catch((e)=>{
console.log("Error",e);
});
}
export { getData };
The code is not entering the catch block. I have also tried setPageCount(pageCount=> pageCount+ 1); and setPageCount(pageCount+ 1); gives same result. What am I doing wrong?
Code Sandbox
Edit: I converted the above code to class based component and it is working fine. I'm more curious on how hooks based approach is resets the states.
import React, { Component } from "react";
export default class InfiniteClass extends Component {
constructor() {
super();
this.state = {
pageCount: 1,
photos: []
};
}
getData = () => {
fetch(
`https://jsonplaceholder.typicode.com/photos?_page=${this.state.pageCount}&limit=3`
)
.then((val) => val.json())
.then((val) => {
this.setState({
photos: [...this.state.photos, ...val],
pageCount: this.state.pageCount + 1
});
})
.catch((e) => {
console.log("Error", e);
});
};
componentDidMount() {
console.log(this.sentinal);
const observer = new IntersectionObserver((entries) => {
if (entries[0].isIntersecting) {
this.getData();
}
});
observer.observe(this.sentinal, { threshold: 1 });
}
render() {
return (
<section>
{this.state &&
this.state.photos.length &&
this.state.photos.map((photo, index) => {
return <img alt="" src={photo.url} key={index} />;
})}
<div
className="sentinal"
ref={(sentinal) => (this.sentinal = sentinal)}
>
Hello
</div>
</section>
);
}
}
Edit 2 : I tried consoling pageCount at two places one above IntersectionCallback and one inside. The value inside is not changing meaning it is storing its own variables.
useState in react takes either argument or function. So, I did something hackish. It is working but I'm looking for a better aproach.
const intersectionCallback = (entries) => {
if (entries[0].isIntersecting) {
setPageCount((pageCount) => {
setData(d=>{
getData(setData, d, pageCount);
return d;
})
return pageCount + 1;
});
}
};

Unable to update react state with an array

I can make a successful call to getApiSuggestions with data returned. However I'm unable to assign this to my state.
As you can see my console output shows that the value for response has an array. However, when attempting to assign it to wikiResults:response the array remains empty.
note that this is a modification of react-search-autocomplete
Am I attempting to pass the variables incorrectly?
NarrativeSearch.js
import React, {useContext, useState, useEffect} from "react";
import './search.css'
import { ReactSearchAutocomplete } from 'react-search-autocomplete'
import { getApiSuggestions } from '../../requests/requests';
import {TextSearchContext} from "../../contexts/TextSearchContext"
import {SearchContext} from "../../contexts/SearchContext"
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const [wikiTitleResults, setWikiTitleResults] = useState({wikiResults:[]});
var cnJson = wikiTitleResults;
const items = wikiTitleResults.wikiResults;
const handleOnSearch = (string, results) => {
console.log("STRING: ", string)
getApiSuggestions(string).then(response => {
console.log("RESPONSE: ", response);
setWikiTitleResults({wikiResults:response}); //<---- This doesn't update the state
console.log("WikiTitle: ", wikiTitleResults.wikiResults);
console.log("Items: ", items);
})
}
const handleOnHover = (result) => {
// the item hovered
console.log(result)
}
const handleOnSelect = (item) => {
// the item selected
setTextFilterState({textFilter:item.name});
console.log(item)
}
const handleOnFocus = () => {
console.log('Focused')
}
const handleOnClear = () => {
setTextFilterState({textFilter:""});
}
const formatResult = (item) => {
return (
<>
<span style={{ display: 'block', textAlign: 'left' }}>id: {item.title}</span>
</>
)
}
return (
<div >
<div className="searchbar">
<ReactSearchAutocomplete
items={items}
onSearch={handleOnSearch}
onHover={handleOnHover}
onSelect={handleOnSelect}
onFocus={handleOnFocus}
onClear={handleOnClear}
styling={{ zIndex: 4 }} // To display it on top of the search box below
autoFocus
/>
</div>
</div>
)
}
export default Search
getApiSuggesetions
const getApiSuggestions = (title) => {
//console.log("URL Being called"+ urlSingleResult);
//console.log(title);
let result = urlMultiResult
.get(`${title}`)
.then((response) => {
console.log(Object.values(response.data.query.pages))
return Object.values(response.data.query.pages);
})
.catch((error) => {
return error;
console.log(error);
});
console.log(result);
return result;
};
I fixed this by including a useEffect and a context from the parent component.
function Search() {
const {textFilterState, setTextFilterState} = useContext(TextSearchContext);
const {wikiTitleResults, setWikiTitleResults} = useContext(SearchContext);
var items = wikiTitleResults.wikiTitles;
useEffect(() => {
const fetchData = async () => {
const data = await getApiSuggestions(textFilterState.textFilter)
setWikiTitleResults({wikiTitles:data})
}
fetchData();
},
[textFilterState])
const handleOnSearch = (string, results) => {
setTextFilterState({textFilter:string});
}

Test component with mock React Jest

This is my component, that contains a Select child component
import { hashHistory } from 'react-router'
import { useEffect } from 'react'
import Select from 'cm/components/common/select'
import { eventTracker, events } from 'cm/common/event-tracker'
import { eventMetricsMap } from './helpers'
import { compose } from 'cm/common/utils'
const SelectLanguage = ({ handlers, languages, selectedLanguage }) => {
useEffect(() => {
hashHistory.listen(({ pathname }) => {
if (pathname.indexOf('/library')) {
handlers.onSelect(null)
}
})
}, [])
const handleSelect = ({ value }) => {
handlers.onSelect(value)
return value.toLowerCase()
}
const trackSelectLanguage = language => {
eventTracker.track(
events.switchBuildingBlickLanguage,
{
from: eventMetricsMap[ 'buildingblocks' ][ 'from' ],
language
}
)
}
return (
<Select
className="search__form--lang dropdown-selection--fixed-width"
handlers={{
onSelect: compose(
trackSelectLanguage,
handleSelect
)
}}
items={languages}
selected={selectedLanguage}
type="medium-input"
/>
)
}
I'd like to make unit test for this component using Jest. My 4 test cases that I want to cover:
renders in the document
renders items
track select open click
handles action
Unfortunately last two cases fails and can't find a reason.
Do you know how to resolve it ?
import expect from 'expect'
import { fireEvent, render } from '#testing-library/react'
import SelectLanguage from './select-language'
const dummyEvents = {
switchBuildingBlockLanguage: 'switch building blocks language'
}
jest.mock(
'cm/common/event-tracker',
() => ({
eventTracker: {
track: () => {}
},
events: {
dummyEvents
}
})
)
const languages = [
{
name: 'English',
value: 'EN',
},
{
name: 'Nederlands',
value: 'NL'
}
]
jest.mock(
'cm/components/common/select',
() => ({ items, handlers }) => {
console.log('items', items)
return (<div className="dropdown">
<div className="dropdown__menu">
{items.map(({ name }) => (
<div className="dropdown__item" onClick={handlers.onSelect}>{name}</div>
))}
</div>
</div>)
}
)
const containerPath = '.dropdown'
const itemPath = '.dropdown__item'
describe('SelectLanguage component', () => {
it('renders in the document', () => {
const { container } = render(
<SelectLanguage languages={languages}/>
)
const $container = container.querySelector(containerPath)
expect($container).toBeTruthy()
})
it('renders items', () => {
const { container } = render(
<SelectLanguage languages={languages}/>
)
const $dropdownItems = container.querySelectorAll(itemPath)
expect([ ...$dropdownItems ].length).toBe(2)
})
it('track select open click', () => {
const spyFn = jest.fn()
const { container } = render(
<SelectLanguage
selectedLanguage={{
value: 'EN'
}}
languages={languages}
handlers={{
onSelect: spyFn
}}
/>
)
const $dropdownItem = container.querySelector(itemPath)
fireEvent.click($dropdownItem)
expect(spyFn).toHaveBeenCalledWith(
dummyEvents.switchBuildingBlockLanguage,
{
from: 'building blocks',
language: 'en'
}
)
})
it('handles action', () => {
const spyFn = jest.fn()
const { container } = render(
<SelectLanguage
selectedLanguage={{
value: 'EN'
}}
languages={languages}
handlers={{
onSelect: spyFn
}}
/>
)
const $dropdownItem = container.querySelector(itemPath)
fireEvent.click($dropdownItem)
expect(spyFn).toHaveBeenCalledWith('EN')
})
})
When using events you need to wait for them to complete before running the assertions.
testing-library/react has the waitFor function:
import { fireEvent, render, waitFor } from '#testing-library/react'
// ...
it('handles action', async () => {
// ...
fireEvent.click($dropdownItem)
await waitFor(() => {
expect(spyFn).toHaveBeenCalledWith(
dummyEvents.switchBuildingBlockLanguage,
{
from: 'building blocks',
language: 'en'
}
)
});
// ...

Pass a JSX element to storybook parameters in a custom build addon

I am building a custom Tab
import React from 'react';
import { addons, types } from '#storybook/addons';
import { AddonPanel } from '#storybook/components';
import { useParameter } from '#storybook/api';
export const ADDON_ID = 'storybook/principles';
export const PANEL_ID = `${ADDON_ID}/panel`;
export const PARAM_KEY = 'principles'; // to communicate from stories
const PanelContent = () => {
const { component: Component } = useParameter(PARAM_KEY, {});
if (!Component) {
return <p>Usage info is missing</p>;
}
return <Component />;
};
addons.register(ADDON_ID, api => {
addons.add(PANEL_ID, {
type: types.Panel,
title: 'Usage',
paramKey: PARAM_KEY,
render: ({ active, key }) => {
return (
<AddonPanel active={active} key={key}>
<PanelContent />
</AddonPanel>
);
},
});
});
& then using it in my stories like
storiesOf('Superman', module)
.addParameters({
component: Superman,
principles: {
component: <Anatomy />
},
})
.add('a story 1', () => <p>some data 1</p>)
.add('a story 2', () => <p>some data 2</p>)
The part where I try to pass in a JSX element like
principles: { component: <Anatomy /> }, // this does not work
principles: { component: 'i can pass in a string' }, // this does work
I get an error like below when I pass in a JSX element as a prop
How can I pass in a JSX element to storybook parameters?
Found a way:
regiter.js
import { deserialize } from 'react-serialize'; //<-- this allows json to jsx conversion
// ...constants definitions
...
const Explanation = () => {
const Explanations = useParameter(PARAM_KEY, null);
const { storyId } = useStorybookState();
const storyKey = storyId.split('--')?.[1];
const ExplanationContent = useMemo(() => {
if (storyKey && Explanations?.[storyKey])
return () => deserialize(JSON.parse(Explanations?.[storyKey]));
return () => <>No extra explanation provided for the selected story</>;
}, [storyKey, Explanations?.[storyKey]]);
return (
<div style={{ margin: 16 }}>
<ExplanationContent />
</div>
);
};
addons.register(ADDON_ID, () => {
addons.add(PANEL_ID, {
type: types.TAB,
title: ADDON_TITLE,
route: ({ storyId, refId }) =>
refId
? `/${ADDON_PATH}/${refId}_${storyId}`
: `/${ADDON_PATH}/${storyId}`,
match: ({ viewMode }) => viewMode === ADDON_PATH,
render: ({ active }) => (active ? <Explanation /> : null),
});
});
and when declaring the parameter:
{
parameters:{
component: serialize(<p>Hello world</p>)
}
}

Redux with Immer not updating component

I'm trying to add an element to an array in an object in my Redux store. I see the object get added to the store but it is not updating the component. If I leave the page and return it is showing up.
I'm pretty sure this is a state mutation issue but I can't figure out where I'm going wrong unless I fundamentally misunderstand what Immer is doing. In the component I'm using produce to add the string to the array, passing the new object to my reducer and using produce to add that object to an array of those objects.
I've looked through a ton of similar questions that all relate to state mutation, but the way I understand it the return from the component's call to produce should be a fully new object. Then in the reducer the call to produce should be returning a new object array.
This is the first time using Immer in a large project so it's entirely possible I don't fully get how it's working it's magic.
Component
import produce from 'immer';
import React, { Component } from 'react';
import { connect } from 'react-redux';
import { Link } from 'react-router-dom';
import uuid from 'uuid/v4';
import { generate } from 'generate-password';
import { updateLeague } from '../../actions/leagues';
import { addTeam } from '../../actions/teams';
import { addUser } from '../../actions/users';
import Team from '../../classes/Team';
import User from '../../classes/User';
import UserWidget from '../utils/user/UserWidget';
class ViewLeague extends Component {
state = {
league : null,
isOwner : false,
owner : '',
teams : [],
inviteEmail: ''
};
componentWillMount() {
console.log('mount props', this.props.leagues);
const { leagues, uuid, leagueId, users, teams } = this.props;
if (leagues.length > 0) {
const league = leagues.find(league => league.uuid === leagueId);
const owner = users.find(user => league.leagueManager === user.uuid);
const leagueTeams = teams.filter(team => league.teams.includes(team.uuid));
this.setState({
league,
isOwner: league.leagueManager === uuid,
owner,
teams : leagueTeams
});
}
}
handleUpdate(event, fieldName) {
this.setState({ [ fieldName ]: event.target.value });
}
findUserByEmail(email) {
//Todo if not found here hit server
return this.props.users.find(user => user.email === email);
}
sendInvite = () => {
const { addTeam, addUser, updateLeague } = this.props;
const { league } = this.state;
const newManager = this.findUserByEmail(this.state.inviteEmail);
const newTeamUuid = uuid();
let newLeague = {};
if (newManager) {
const newTeam = new Team('New Team', newManager.uuid, newTeamUuid);
addTeam(newTeam);
} else {
const newPass = generate({
length : 10,
number : true,
uppercase: true,
strict : true
});
const newUserUuid = uuid();
const newUser = new User('', this.state.inviteEmail, newPass, '', '', newUserUuid);
addUser(newUser);
const newTeam = new Team('New Team', newUserUuid, newTeamUuid);
addTeam(newTeam);
newLeague = produce(league, draft => {draft.teams.push(newTeamUuid);});
updateLeague(newLeague);
console.log('invite props', this.props);
console.log('league same', league === newLeague);
}
//Todo handle sending email invite send password and link to new team
console.log('Invite a friend', this.state.inviteEmail);
};
renderInvite() {
const { isOwner, league, teams } = this.state;
if (isOwner) {
if ((league.leagueType === 'draft' && teams.length < 8) || league.leagueType !== 'draft') {
return (
<div>
<p>You have an empty team slot. Invite a fried to join!</p>
<input type="text"
placeholder={'email'}
onChange={() => this.handleUpdate(event, 'inviteEmail')}/>
<button onClick={this.sendInvite}>Invite</button>
</div>
);
}
}
}
renderViewLeague() {
console.log('render props', this.props.leagues);
const { league, owner, teams } = this.state;
const editLink = this.state.isOwner ?
<Link to={`/leagues/edit/${this.props.leagueId}`}>Edit</Link> :
'';
return (
<div>
<h2>{league.leagueName} </h2>
<h3>League Manager: <UserWidget user={owner}/> - {editLink}</h3>
<p>League Type: {league.leagueType}</p>
{this.renderInvite()}
<br/>
<hr/>
<h2>Teams</h2>
<span>{teams.map((team) => (<p key={team.uuid}>{team.teamName}</p>))}</span>
<span>
<h2>Scoring: </h2>
{league.scoring.map((score, index) => (
<p key={index}>{`Round ${index + 1}: ${score} points`}</p>
)
)}
</span>
</div>
);
}
render() {
if (!this.state.league) {
return (
<div>
<h2>No league Found</h2>
</div>
);
} else {
return (
<div>
{this.renderViewLeague()}
</div>
);
}
}
}
export default connect(
({ leagues: { leagues }, teams: { teams }, users: { users }, auth: { uuid } },
{ match: { params: { leagueId } } }) => ({
leagues,
teams,
users,
uuid,
leagueId
}), ({
addTeam : (team) => addTeam(team),
addUser : (user) => addUser(user),
updateLeague: (league) => updateLeague(league)
})
)(ViewLeague);
Reducer
import produce from 'immer';
import {
ADD_LEAGUE,
UPDATE_LEAGUE
} from '../actions/types';
const DEFAULT_LEAGUES = {
leagues: [ {
leagueName : 'Test League',
leagueManager: 'testUser12345',
uuid : 'testLeague12345',
teams : [ 'testTeam12345', 'testTeam23456' ],
scoring : [ 25, 20, 15, 10, 5, -5 ],
leagueType : 'draft'
} ]
};
const leaguesReducer = (state = DEFAULT_LEAGUES, action) =>
produce(state, draft => {
// noinspection FallThroughInSwitchStatementJS
switch (action.type) {
case ADD_LEAGUE:
draft.leagues.push(action.league);
case UPDATE_LEAGUE:
console.log('updating league', action.league);
const { league } = action;
const leagueIndex = draft.leagues.findIndex(fLeague => league.uuid === fLeague.uuid);
draft.leagues.splice(leagueIndex, 1, league);
}
});
export default leaguesReducer;
Any help is greatly appreciated!! More info available if needed
Try adding return; at the end of your case blocks.
You can read more about returning data from producers and see examples of what to do and what not to do here.

Resources