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.
Related
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.
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
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>)
}
}
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").
I'm having a little problem. Being a beginner in "react apollo ...", I want to pass the value of my state "selectCarcolor" as parameter of my query.This must be done when I select a color from the drop-down list.I read a lot of things in the documentation but I do not know where to start.
You can see all of my code here: Github link description here
onChangeCarColor(e){
//const selectCarcolor = this.state.selectCarcolor
this.setState({ selectCarcolor:e.target.value})
console.log("color " + this.state.selectCarcolor);
}
const Cquery = `gql query getAllUsers($color: String!) {
getAllUsers(color: $color) {
_id
name
cars {
color
}
}
}`;
const datafetch = graphql(Cquery, {
options: props=> ({
variables: { color: **Here I want to pass the select value**},
})
});
Hoping for a little help from you.
Thank you guys!
react version :16.2.0
react-apollo version : 2.0.1
You can wrap your component with withApollo function from react-apollo package which injects the ApolloClient into your component (available as this.props.client). You can send a query using it. Check official docs and this tutorial for more details and explanations.
Example:
import React, { Component } from 'react'
import { withApollo } from 'react-apollo'
import gql from 'graphql-tag'
import Link from './Link'
class Search extends Component {
state = {
links: [],
filter: ''
}
render() {
return (
<div>
<div>
Search
<input
type='text'
onChange={(e) => this.setState({ filter: e.target.value })}
/>
<button
onClick={() => this._executeSearch()}
>
OK
</button>
</div>
{this.state.links.map((link, index) => <Link key={link.id} link={link} index={index}/>)}
</div>
)
}
_executeSearch = async () => {
const { filter } = this.state
const result = await this.props.client.query({
query: FEED_SEARCH_QUERY,
variables: { filter },
})
const links = result.data.feed.links
this.setState({ links })
}
}
const FEED_SEARCH_QUERY = gql`
query FeedSearchQuery($filter: String!) {
feed(filter: $filter) {
links {
id
url
description
createdAt
postedBy {
id
name
}
votes {
id
user {
id
}
}
}
}
}
`
export default withApollo(Search)
In your datafetch() function you are setting option variables in a function on props, but you are setting your color selected to your state.
Can you just do
options: {
variables: { color: this.state.selectCarcolor, }
}
instead of what you are doing?