Why is AsyncSelect only showing options once? - reactjs

I'm trying to make use of <AsyncSelect /> to pull options from an API to populate a form.
It works find the first time I search for something, I can select the option and am then prompted to type again.
However when I type something else, I get No Options even though I can see the data show up in a console.log().
import React, { Component } from 'react';
import AsyncSelect from 'react-select/async';
type State = {
inputValue: string,
};
const promiseOptions = inputValue =>
new Promise(resolve => {
setTimeout(() => {
resolve(
fetch('https://restcountries.eu/rest/v2/all')
.then(r => r.json())
.then(rjson => {
return rjson.map(c => {
return { name: c.name, label: c.name}
})
})
);
}, 1000);
});
export default class AsyncMulti extends Component<*, State> {
state = { inputValue: '' };
handleInputChange = (newValue: string) => {
const inputValue = newValue.replace(/\W/g, '');
this.setState({ inputValue });
return inputValue;
};
render() {
return (
<AsyncSelect
isMulti
loadOptions={promiseOptions}
/>
);
}
}

I just realized that the object being returned in the Promise:
{ name: c.name, label: c.name }
had the wrong keys, and should have been:
{ value: c.name, label: c.name }
And it works now.

Related

Testing React Context update

I have been struggling to test a React Context update for a while. I can predefine the value of the Context.Provider however, the solution is not ideal because an update to the context which is supposed to happen within component utilising the context is not actually happening.
When I test this manually the text 'Account name: abc' changes to 'Account name: New account name' but not in the test. The context value remains the same.
The reason I predefine the value is because the component relies on a fetch response from a parent and I am unit testing <ImportAccounts /> only.
In a test I am rendering a component with predefined value
test('accountName value is updated on click', () => {
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
expect(getByText('Account name: abc')).toBeInTheDocument();
const input = getByLabelText('Account name');
fireEvent.change(input, { target: { value: 'New account name' } });
fireEvent.click(getByRole('button', { name: 'Update' }));
expect(getByText('Account name: New account name')).toBeInTheDocument();
});
Here's my context
import React, { createContext, useContext, useState, useCallback, Dispatch, SetStateAction } from 'react';
export interface StateVariable<T> {
value: T;
setValue: Dispatch<SetStateAction<T>>;
}
export interface ImportAccountsState {
accountName: StateVariable<string>;
}
export const ImportAccountsContext = createContext<ImportAccountsState>(
{} as ImportAccountsState,
);
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};
Import Accounts is as simple as
export const ImportAccounts = () => {
const { accountName } = useImportAccountsContext();
const [newAccountName, setNewAccountName] = useState(accountName.value);
const handleAccountNameChange = () => {
accountName.setValue(newAccountName);
};
return (
<>
<h1>Account name: {accountName.value}</h1>
<label htmlFor="accountName">Account name</label>
<input
value={newAccountName}
onChange={e => setNewAccountName(e.target.value)}
id="accountName"
/>
<button
type="button"
onClick={handleAccountNameChange}>
Update
</button>
</>
);
}
How can I test that accountName has actually updated?
If we don't need the default value for the ImportAccounts provider then we make the test pass easily. ImportAccountsProvider manages the state of the accountName within itself. In that provider, we are passing the accountName state of type ImportAccountsState to all our children through the context provider.
Now coming to your problem,
const { getByText, getByLabelText, getByRole } = render(
<ImportAccountsContext.Provider value={{ accountName: { value: 'abc', setValue: jest.fn() }}}>
<ImportAccounts />
</ImportAccountsContext.Provider>,
);
Here, the value: 'abc' is not a state value, it's simply a string constant 'abc' which will never be going to change. This is something that we should note. We must pass the state value to the context provider if we want to share the value with the children which is not going to be constant in the entire react lifecycle.
import { render, screen } from '#testing-library/react';
import userEvent from '#testing-library/user-event';
test('should update the context with existing provider', () => {
render(
<ImportAccountsProvider>
<ImportAccounts />
</ImportAccountsProvider>
);
// some constant hoisted to make it clean code
const accountInput = screen.getByRole('textbox', { name: /account name/i });
const accountInputValue = 'subrato patnaik';
expect(accountInput).toHaveAttribute('value', '');
// do some changes
userEvent.type(accountInput, accountInputValue);
//validate "changes"
expect(screen.getByDisplayValue(accountInputValue)).toBeTruthy();
expect(accountInput).toHaveAttribute('value', accountInputValue);
// update context
userEvent.click(screen.getByRole('button', { name: /update/i }));
// validate update
expect(screen.getByRole('heading')).toHaveTextContent(/subrato/i);
screen.debug();
});
Inside the ImportAccountsProvider we can do the fetch call and set the accountName state to the response of the fetch call.
export const ImportAccountsProvider = ({ children }: { children: React.ReactNode }) => {
const [accountName, setAccountName] = useState('');
useEffect(() => {
// do the fetch call here and update the accountName state accordingly
});
const initialState: ImportAccountsState = {
accountName: {
value: accountName,
setValue: setAccountName,
},
};
return (
<ImportAccountsContext.Provider value={initialState}>
{children}
</ImportAccountsContext.Provider>
);
};
export const useImportAccountsContext = () => {
return useContext<ImportAccountsState>(ImportAccountsContext);
};

React Typescript passing props and data type

I'm still pretty new into the world of TypeScript and I have a couple of questions. I have a few errors in my code, and I'm not rlly sure how to pass the props and choose the right type. I would appreciate a bit of help on that one.
Do you have maybe a good source where I can find all the necessary React info in one place just for the start?
tl;dr
What will be the { data } type? How to define it?
How to pass the functions as props to the Results.tsx file? How define result, results and openPopup in this function?
Did I miss something else?
App.tsx
import React, { useState } from 'react'
import axios from 'axios'
import Search from './components/Search'
import Results from './components/Results'
import Popup from './components/Popup'
export type selected = {
Title?: string,
Year?:number
}
type values = {
s: string,
results: string[],
selected: selected,
}
interface popup {
id: string
}
const App: React.FC = () => {
const [state, setState] = useState<values>({
s: "",
results: [],
selected: {}
});
const apiurl = "http://www.omdbapi.com/?apikey=dfe6d885";
const search = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === "Enter") {
axios(apiurl + "&s=" + state.s).then(({ data }) => {
let results = data.Search;
setState(prevState => {
return { ...prevState, results: results }
})
});
}
}
const handleInput = (e: React.ChangeEvent<HTMLInputElement>) => {
let s = e.target.value;
setState(prevState => {
return { ...prevState, s: s }
});
}
const openPopup = (id : string) => {
axios(apiurl + "&i=" + id).then(({ data }) => {
let result = data;
console.log(result);
setState(prevState => {
return { ...prevState, selected: result }
});
});
}
const closePopup = () => {
setState(prevState => {
return { ...prevState, selected: {} }
});
}
return (
<div className="App">
<header>
<h1>Movie Database</h1>
</header>
<main>
<Search handleInput={handleInput} search={search} />
<Results results={state.results} openPopup={openPopup} />
{(typeof state.selected.Title != "undefined") ? <Popup selected={state.selected} closePopup={closePopup} /> : false}
</main>
</div>
);
}
export default App
Results.tsx
import React from 'react'
import Result from './Result'
function Results ({ results, openPopup }) {
return (
<section className="results">
{results.map(result => (
<Result key={result.imdbID} result={result} openPopup={openPopup} />
))}
</section>
)
}
export default Results
Prop Types
You can defined the props that you pass to a function component inline
function Results ({ results, openPopup }: { results: MyResult[], openPopup: () => void }) {
But it's more common to define a separate interface for the props
interface Props {
results: MyResult[];
openPopup: () => void;
}
Which you can use to define the props directly
function Results ({ results, openPopup }: Props) {
Or through React.FC
const Results: React.FC<Props> = ({ results, openPopup }) => {
Data Type
To define the type for data you need to create an interface that has all of the properties in the API response that you want to use. Then when you call axios, you use the generic type of the axios function to tell it what the fetched data should look like.
For whatever reason, axios() doesn't seem to take a generic, but axios.get() does.
axios.get<MyApiResponse>(apiurl + "&s=" + state.s).then(({ data }) => {
Now the data variable automatically has the MyApiResponse type.

unable to set react-select-async-paginate initial value

I am using a react-select-async-paginate. It's working on form submit. I am facing problem in edit form. When I click on edit button, I am able to show all the values in their tags except the select. I have made a few attempts to show the selected value but no luck.
export function UserDropdown(props) {
const [value, setValue] = useState(null);
const defaultAdditional = {
page: 1
};
const onChange = (e) => {
props.getUserEmail(e)
setValue(e)
}
<AsyncPaginate
additional={defaultAdditional}
defaultValue={props.value}
loadOptions={loadPageOptions}
onChange={ (e) => onChange(e)}
/>
}
import UserDropdown from './userDropdown'
export class User extends Component {
constructor(props) {
super(props);
this.state = {
userEmail: ''
}
}
onUserChange(e) {
this.setState({userEmail: e.value})
}
<UserDropdown getUserEmail = { (email) => this.onUserChange(email)} value={this.state.userEmail} />
}
in loadPageOptions I am using the API's response.
If you want to give a controlled select a default value you need to make sure that value initially is the default value. You can only use defaultValue if the select is uncontrolled meaning that it manages state of value internally:
const OPTIONS = [
{
value: 1,
label: "Audi"
},
{
value: 2,
label: "Mercedes"
},
{
value: 3,
label: "BMW"
}
];
const fakeData = async () => {
await new Promise(resolve => setTimeout(resolve, 1000));
return {
results: OPTIONS,
has_more: false
};
};
const loadOptions = async () => {
const response = await fakeData();
return {
options: response.results,
hasMore: response.has_more
};
};
export default function App() {
// this has to be initialized with the default value
const [value, setValue] = useState({ value: 1, label: "Audi" });
return (
<div className="App">
<AsyncPaginate
value={value}
loadOptions={loadOptions}
onChange={setValue}
/>
</div>
);
}

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.

React-Select with React-Apollo does not work

We are using react-select and fetching the items as the user types. I am not able to make it work with react-apollo.
Can someone help me provide a guideline?
Here is my unsuccessful attempt:
class PatientSearchByPhone extends Component {
updateProp = mobile => {
if (mobile.length < 10) return;
this.props.data.refetch({ input: { mobile } });
};
render() {
console.log(this.props.data);
return <AsyncSelect cacheOptions loadOptions={this.updateProp} />;
}
}
const FETCH_PATIENT = gql`
query Patient($input: PatientSearchInput) {
getPatients(input: $input) {
id
first_name
}
}
`;
export default graphql(FETCH_PATIENT, {
options: ({ mobile }) => ({ variables: { input: { mobile } } })
})(PatientSearchByPhone);
Versions:
"react-apollo": "^2.1.11",
"react-select": "^2.1.0"
Thanks for your time.
I got an e-mail asking a response to this question. It reminds me of this XKCD comics:
I do not recall the exact solution I implemented, so I setup a complete example for this.
This app (code snippet below) kickstarts searching as soon as you type 4 characters or more in the input box (You are expected to type artist's name. Try vinci?). Here is the code:
import React, { useState } from "react";
import "./App.css";
import AsyncSelect from "react-select/async";
import ApolloClient, { gql } from "apollo-boost";
const client = new ApolloClient({
uri: "https://metaphysics-production.artsy.net"
});
const fetchArtists = async (input: string, cb: any) => {
if (input && input.trim().length < 4) {
return [];
}
const res = await client.query({
query: gql`
query {
match_artist(term: "${input}") {
name
imageUrl
}
}
`
});
if (res.data && res.data.match_artist) {
return res.data.match_artist.map(
(a: { name: string; imageUrl: string }) => ({
label: a.name,
value: a.imageUrl
})
);
}
return [];
};
const App: React.FC = () => {
const [artist, setArtist] = useState({
label: "No Name",
value: "https://dummyimage.com/200x200/000/fff&text=No+Artist"
});
return (
<div className="App">
<header className="App-header">
<h4>Search artists and their image (type 4 char or more)</h4>
<AsyncSelect
loadOptions={fetchArtists}
onChange={(opt: any) => setArtist(opt)}
placeholder="Search an Artist"
className="select"
/>
<div>
<img alt={artist.label} src={artist.value} className="aimage" />
</div>
</header>
</div>
);
};
export default App;
You can clone https://github.com/naishe/react-select-apollo it is a working example. I have deployed the app here: https://apollo-select.naishe.in/, may be play a little?
The other option is to execute the graphql query manually using the client that is exposed by wrapping the base component with withApollo.
In the example below, we have,
BaseComponnent which renders the AsyncSelect react-select component
loadOptionsIndexes which executes the async graphql fetch via the client
BaseComponent.propTypes describes the required client prop
withApollo wraps the base component to give us the actual component we'll use elsewhere in the react app.
const BaseComponent = (props) => {
const loadOptionsIndexes = (inputValue) => {
let graphqlQueryExpression = {
query: QUERY_INDEXES,
variables: {
name: inputValue
}
}
const transformDataIntoValueLabel = (data) => {
return data.indexes.indexes.map(ix => { return { value: ix.id, label: ix.name }})
}
return new Promise(resolve => {
props.client.query(graphqlQueryExpression).then(response => {
resolve(transformDataIntoValueLabel(response.data))
})
});
}
return (
<>
<div className="chart-buttons-default">
<div className="select-index-input" style={{width: 400, display: "inline-block"}}>
<AsyncSelect
isMulti={true}
cacheOptions={true}
defaultOptions={true}
loadOptions={loadOptionsIndexes} />
</div>
</div>
</>
)
}
BaseComponent.propTypes = {
client: PropTypes.any,
}
const ComplementComponent = withApollo(BaseComponent);
Sorry if the example is a little off - copy and pasted what I had working rather than moving on without giving back.

Resources