Relay Error when deleting: RelayMutationQuery: Invalid field name on fat query - javascript

I'm running into an issue when I attempt to commit a deletion mutation. When I commit, I get the error Uncaught Invariant Violation: RelayMutationQuery: Invalid field name on fat query, `company`.. Viewing, creating and updating nodes all work. For some reason I just can't delete. It mentions the company field in the fatQuery, but the only field I have in the fat query is the deletedUserId I get back from the server. Thanks in advance!
Component:
import React, {Component} from 'react';
import Relay from 'react-relay';
import {Link} from 'react-router';
import DeleteUserMutation from 'mutations/DeleteUserMutation';
import styles from './EmployeeItem.css';
class EmployeeItem extends Component {
render() {
const {user} = this.props;
return (
<div className={styles.employee}>
<p><strong>ID:</strong> {user.id}</p>
<p><strong>First Name:</strong> {user.firstName}</p>
<p><strong>Last Name:</strong> {user.lastName}</p>
<p><strong>Email:</strong> {user.email}</p>
<div className="btn-group">
<Link to={`/company/employees/${user.id}`} className="btn btn-primary">View Employee</Link>
<button onClick={this.handleRemove} className="btn btn-danger">Delete User</button>
</div>
</div>
)
}
handleRemove = (e) => {
e.preventDefault();
const {user, company} = this.props;
Relay.Store.commitUpdate(new DeleteUserMutation({user, company}));
};
}
export default Relay.createContainer(EmployeeItem, {
fragments: {
company: () => Relay.QL`
fragment on Company {
id
${DeleteUserMutation.getFragment('company')}
}
`,
user: () => Relay.QL`
fragment on User {
id
firstName
lastName
email
${DeleteUserMutation.getFragment('user')}
}
`
}
});
Mutation:
import React from 'react';
import Relay from 'react-relay';
export default class DeleteUserMutation extends Relay.Mutation {
static fragments = {
company: () => Relay.QL`
fragment on Company {
id
}
`,
user: () => Relay.QL`
fragment on User {
id
}
`
};
getMutation() {
return Relay.QL`mutation {deleteUser}`;
}
getFatQuery() {
return Relay.QL`
fragment on DeleteUserPayload {
deletedUserId
}
`;
}
getVariables() {
return {
id: this.props.user.id,
}
}
getConfigs() {
return [{
type: 'NODE_DELETE',
parentName: 'company',
parentID: this.props.company.id,
connectionName: 'employees',
deletedIDFieldName: 'deletedUserId'
}]
}
// Wasn't sure if this was causing the error but it appears to be
// something else.
// getOptimisticResponse() {
// return {
// deletedUserId: this.props.user.id
// }
// }
}

This error is referring to the fact that you reference the "company" in your getConfigs() implementation. The NODE_DELETE config tells Relay how to construct the mutation query by mapping nodes in the store (e.g. parentID) to fields on the fat query (e.g. parentName).
Although you might not necessarily need it today, you should add the company to the mutation payload & fat query here, since the company is being affected by this change. More specifically, the company's employees connection is being modified :)

NevilleS' solution solved it for me:
I added a globalId to the root field (in my case an object called "verify") and I also changed my mutation on the server to return an edge, rather than just the underlying type. I also added the root "verify" object to the mutation output fields: it would make sense that the client's relay mutation needs that to know which object owns the connection, where to put the new edge.
export const Verify = new GraphQLObjectType({
name: 'Verify',
fields: () => ({
id: globalIdField('Verify'),
verifications: {
args: connectionArgs,
type: VerificationConnection,
resolve: (rootValue, args) => connectionFromArray(rootValue.verifications, args)
},
Adding "verify" and "verificationEdge" to the mutation's output fields.
export const AddVerifiedSchool = mutationWithClientMutationId({
name: 'AddVerifiedSchool',
inputFields: {
verification: {
type: VerifiedSchoolInput
}
},
outputFields: {
success: {
type: GraphQLBoolean,
resolve: () => true
},
verificationEdge: {
type: VerificationEdge,
resolve: ({verification, context}) => {
console.log('verification', verification);
return verification
}
},
verify: {
type: Verify,
resolve: ({verification, context}) => {
return context.rootValue
}
}
},
Adding the verify field to the fat query, and (the globalId "id" from verify) to the fragments, and using the new globalId to identify the node where the connection exists.
static fragments = {
verify: () => Relay.QL`fragment on Verify { id }`,
action: () => Relay.QL`fragment on Action { name url }`
};
getConfigs() {
return [{
type: 'RANGE_ADD',
parentName: 'verify',
parentID: this.props.verify.id,
connectionName: 'verifications',
edgeName: 'verificationEdge',
rangeBehaviors: {
'': 'append'
}
}];
}
getFatQuery() {
return Relay.QL`
fragment on AddVerifiedSchoolPayload {
verification {
${VerifiedSchool.getFragment('verification')}
}
verify {
id
}
}`
}

Related

merge previous data while scrolling on relay pagination graphql

I am trying to use relay style pagination. However, I am getting trouble on infinite scrolling. When i scroll or load next sets of data then I just get the current data without it being merged to the previous data. This is how I have done
cache.ts
import { InMemoryCache } from '#apollo/client';
import { relayStylePagination } from '#apollo/client/utilities';
const cache = new InMemoryCache({
typePolicies: {
Query: {
fields: {
conversation: relayStylePagination(),
},
},
},
});
export default cache;
Conversation Query
In my case, params like first, after, before, last are inside params object
export const CONVERSATION = gql`
query conversation($channel: ShortId, $contact: ShortId, $params: ConnectionInput) {
conversation(channel: $channel, contact: $contact, params: $params) {
status
data {
pageInfo {
...PageInfo
}
edges {
cursor
node {
...Communication
}
}
}
}
}
${PAGE_INFO}
${COMMUNICATION}
`;
Conversation.tsx
const [loadConversation, { data, fetchMore, networkStatus, subscribeToMore }] = useLazyQuery(
CONVERSATION,
);
useEffect(() => {
isMounted.current = true;
if (channelId && contactId) {
loadConversation({
variables: {
channel: channelId,
contact: contactId,
params: { first },
},
});
}
return () => {
isMounted.current = false;
};
}, [channelId, contactId, loadConversation]);
<React.Suspense fallback={<Spinner />}>
<MessageList messages={messages ? generateChatMessages(messages) : []} />
{hasNextPage && (
<>
<button
type='button'
ref={setButtonRef}
id='buttonLoadMore'
disabled={isRefetching}
onClick={() => {
if (fetchMore) {
fetchMore({
variables: {
params: {
first,
after: data?.conversation?.data?.pageInfo.endCursor,
},
},
});
}
}}
/>
</>
)}
</React.Suspense>
Can I know what I have missed?
The first, after, before, last should be declared as arguments of conversation rather than as properties of params.
Apollo merges the previous pages when the query arguments contain after/before .
query conversation($channel: ShortId, $contact: ShortId, $after: String, $first: Int, $before: String, $last: Int) {
conversation(channel: $channel, contact: $contact, after: $after, first: $first, before: $before, last: $last) {
...
}
}

How to show submitted data in React immediately, without refreshing the page?

I'm building a simple note-taking app and I'm trying to add new note at the end of the list of notes, and then see the added note immediately. Unfortunately I'm only able to do it by refreshing the page. Is there an easier way?
I know that changing state would usually help, but I have two separate components and I don't know how to connect them in any way.
So in the NewNoteForm component I have this submit action:
doSubmit = async () => {
await saveNote(this.state.data);
};
And then in the main component I simply pass the NewNoteForm component.
Here's the whole NewNoteForm component:
import React from "react";
import Joi from "joi-browser";
import Form from "./common/form";
import { getNote, saveNote } from "../services/noteService";
import { getFolders } from "../services/folderService";
class NewNoteForm extends Form {
//extends Form to get validation and handling
state = {
data: {
title: "default title",
content: "jasjdhajhdjshdjahjahdjh",
folderId: "5d6131ad65ee332060bfd9ea"
},
folders: [],
errors: {}
};
schema = {
_id: Joi.string(),
title: Joi.string().label("Title"),
content: Joi.string()
.required()
.label("Note"),
folderId: Joi.string()
.required()
.label("Folder")
};
async populateFolders() {
const { data: folders } = await getFolders();
this.setState({ folders });
}
async populateNote() {
try {
const noteId = this.props.match.params.id;
if (noteId === "new") return;
const { data: note } = await getNote(noteId);
this.setState({ data: this.mapToViewModel(note) });
} catch (ex) {
if (ex.response && ex.response.status === 404)
this.props.history.replace("/not-found");
}
}
async componentDidMount() {
await this.populateFolders();
await this.populateNote();
}
mapToViewModel(note) {
return {
_id: note._id,
title: note.title,
content: note.content,
folderId: note.folder._id
};
}
scrollToBottom = () => {
this.messagesEnd.scrollIntoView({ behavior: "smooth" });
}
doSubmit = async () => {
await saveNote(this.state.data);
};
render() {
return (
<div>
<h1>Add new note</h1>
<form onSubmit={this.handleSubmit}>
{this.renderSelect("folderId", "Folder", this.state.folders)}
{this.renderInput("title", "Title")}
{this.renderInput("content", "Content")}
{this.renderButton("Add")}
</form>
</div>
);
}
}
export default NewNoteForm;
And here's the whole main component:
import React, { Component } from "react";
import { getNotes, deleteNote } from "../services/noteService";
import ListGroup from "./common/listGroup";
import { getFolders } from "../services/folderService";
import { toast } from "react-toastify";
import SingleNote from "./singleNote";
import NewNoteForm from "./newNoteForm";
class Notes extends Component {
state = {
notes: [], //I initialize them here so they are not undefined while componentDidMount is rendering them, otherwise I'd get a runtime error
folders: [],
selectedFolder: null
};
async componentDidMount() {
const { data } = await getFolders();
const folders = [{ _id: "", name: "All notes" }, ...data];
const { data: notes } = await getNotes();
this.setState({ notes, folders });
}
handleDelete = async note => {
const originalNotes = this.state.notes;
const notes = originalNotes.filter(n => n._id !== note._id);
this.setState({ notes });
try {
await deleteNote(note._id);
} catch (ex) {
if (ex.response && ex.response.status === 404)
toast.error("This note has already been deleted.");
this.setState({ notes: originalNotes });
}
};
handleFolderSelect = folder => {
this.setState({ selectedFolder: folder }); //here I say that this is a selected folder
};
render() {
const { selectedFolder, notes } = this.state;
const filteredNotes =
selectedFolder && selectedFolder._id //if the selected folder is truthy I get all the notes with this folder id, otherwise I get all the notes
? notes.filter(n => n.folder._id === selectedFolder._id)
: notes;
return (
<div className="row m-0">
<div className="col-3">
<ListGroup
items={this.state.folders}
selectedItem={this.state.selectedFolder} //here I say that this is a selected folder
onItemSelect={this.handleFolderSelect}
/>
</div>
<div className="col">
<SingleNote
filteredNotes={filteredNotes}
onDelete={this.handleDelete}
/>
<NewNoteForm />
</div>
</div>
);
}
}
export default Notes;
How can I connect these two components so that the data shows smoothly after submitting?
You can use a callback-like pattern to communicate between a child component and its parent (which is the 3rd strategy in #FrankerZ's link)
src: https://medium.com/#thejasonfile/callback-functions-in-react-e822ebede766)
Essentially you pass in a function into the child component (in the main/parent component = "Notes": <NewNoteForm onNewNoteCreated={this.onNewNoteCreated} />
where onNewNoteCreated can accept something like the new note (raw data or the response from the service) as a parameter and saves it into the parent's local state which is in turn consumed by any interested child components, i.e. ListGroup).
Sample onNewNoteCreated implementation:
onNewNoteCreated = (newNote) => {
this.setState({
notes: [...this.state.notes, newNote],
});
}
Sample use in NewNoteForm component:
doSubmit/handleSubmit = async (event) => {
event.preventDefault();
event.stopPropagation();
const newNote = await saveNote(this.state.data);
this.props.onNewNoteCreated(newNote);
}
You probably want to stop the refresh of the page on form submit with event.preventDefault() and event.stopPropagation() inside your submit handler (What's the difference between event.stopPropagation and event.preventDefault?).

confused about reactjs lifecycle

I have the following Sharepoint Framework webpart, which basically renders a grid depending on a dropdown property which has the list names.
import * as React from "react";
import * as ReactDom from "react-dom";
import { Version } from "#microsoft/sp-core-library";
import {
BaseClientSideWebPart,
IPropertyPaneConfiguration,
PropertyPaneTextField,
PropertyPaneDropdown,
IPropertyPaneDropdownOption,
IPropertyPaneField,
PropertyPaneLabel
} from "#microsoft/sp-webpart-base";
import * as strings from "FactoryMethodWebPartStrings";
import FactoryMethod from "./components/FactoryMethod";
import { IFactoryMethodProps } from "./components/IFactoryMethodProps";
import { IFactoryMethodWebPartProps } from "./IFactoryMethodWebPartProps";
import * as lodash from "#microsoft/sp-lodash-subset";
import List from "./components/models/List";
import { Environment, EnvironmentType } from "#microsoft/sp-core-library";
import IDataProvider from "./components/dataproviders/IDataProvider";
import MockDataProvider from "./test/MockDataProvider";
import SharePointDataProvider from "./components/dataproviders/SharepointDataProvider";
export default class FactoryMethodWebPart extends BaseClientSideWebPart<IFactoryMethodWebPartProps> {
private _dropdownOptions: IPropertyPaneDropdownOption[];
private _selectedList: List;
private _disableDropdown: boolean;
private _dataProvider: IDataProvider;
private _factorymethodContainerComponent: FactoryMethod;
protected onInit(): Promise<void> {
this.context.statusRenderer.displayLoadingIndicator(this.domElement, "Todo");
/*
Create the appropriate data provider depending on where the web part is running.
The DEBUG flag will ensure the mock data provider is not bundled with the web part when you package the
solution for distribution, that is, using the --ship flag with the package-solution gulp command.
*/
if (DEBUG && Environment.type === EnvironmentType.Local) {
this._dataProvider = new MockDataProvider();
} else {
this._dataProvider = new SharePointDataProvider();
this._dataProvider.webPartContext = this.context;
}
this.openPropertyPane = this.openPropertyPane.bind(this);
/*
Get the list of tasks lists from the current site and populate the property pane dropdown field with the values.
*/
this.loadLists()
.then(() => {
/*
If a list is already selected, then we would have stored the list Id in the associated web part property.
So, check to see if we do have a selected list for the web part. If we do, then we set that as the selected list
in the property pane dropdown field.
*/
if (this.properties.spListIndex) {
this.setSelectedList(this.properties.spListIndex.toString());
this.context.statusRenderer.clearLoadingIndicator(this.domElement);
}
});
return super.onInit();
}
// render method of the webpart, actually calls Component
public render(): void {
const element: React.ReactElement<IFactoryMethodProps > = React.createElement(
FactoryMethod,
{
spHttpClient: this.context.spHttpClient,
siteUrl: this.context.pageContext.web.absoluteUrl,
listName: this._dataProvider.selectedList === undefined ? "GenericList" : this._dataProvider.selectedList.Title,
dataProvider: this._dataProvider,
configureStartCallback: this.openPropertyPane
}
);
// reactDom.render(element, this.domElement);
this._factorymethodContainerComponent = <FactoryMethod>ReactDom.render(element, this.domElement);
}
// loads lists from the site and fill the dropdown.
private loadLists(): Promise<any> {
return this._dataProvider.getLists()
.then((lists: List[]) => {
// disable dropdown field if there are no results from the server.
this._disableDropdown = lists.length === 0;
if (lists.length !== 0) {
this._dropdownOptions = lists.map((list: List) => {
return {
key: list.Id,
text: list.Title
};
});
}
});
}
protected get dataVersion(): Version {
return Version.parse("1.0");
}
protected onPropertyPaneFieldChanged(propertyPath: string, oldValue: any, newValue: any): void {
/*
Check the property path to see which property pane feld changed. If the property path matches the dropdown, then we set that list
as the selected list for the web part.
*/
if (propertyPath === "spListIndex") {
this.setSelectedList(newValue);
}
/*
Finally, tell property pane to re-render the web part.
This is valid for reactive property pane.
*/
super.onPropertyPaneFieldChanged(propertyPath, oldValue, newValue);
}
// sets the selected list based on the selection from the dropdownlist
private setSelectedList(value: string): void {
const selectedIndex: number = lodash.findIndex(this._dropdownOptions,
(item: IPropertyPaneDropdownOption) => item.key === value
);
const selectedDropDownOption: IPropertyPaneDropdownOption = this._dropdownOptions[selectedIndex];
if (selectedDropDownOption) {
this._selectedList = {
Title: selectedDropDownOption.text,
Id: selectedDropDownOption.key.toString()
};
this._dataProvider.selectedList = this._selectedList;
}
}
// we add fields dynamically to the property pane, in this case its only the list field which we will render
private getGroupFields(): IPropertyPaneField<any>[] {
const fields: IPropertyPaneField<any>[] = [];
// we add the options from the dropdownoptions variable that was populated during init to the dropdown here.
fields.push(PropertyPaneDropdown("spListIndex", {
label: "Select a list",
disabled: this._disableDropdown,
options: this._dropdownOptions
}));
/*
When we do not have any lists returned from the server, we disable the dropdown. If that is the case,
we also add a label field displaying the appropriate message.
*/
if (this._disableDropdown) {
fields.push(PropertyPaneLabel(null, {
text: "Could not find tasks lists in your site. Create one or more tasks list and then try using the web part."
}));
}
return fields;
}
private openPropertyPane(): void {
this.context.propertyPane.open();
}
protected getPropertyPaneConfiguration(): IPropertyPaneConfiguration {
return {
pages: [
{
header: {
description: strings.PropertyPaneDescription
},
groups: [
{
groupName: strings.BasicGroupName,
/*
Instead of creating the fields here, we call a method that will return the set of property fields to render.
*/
groupFields: this.getGroupFields()
}
]
}
]
};
}
}
my component.tsx
//#region Imports
import * as React from "react";
import styles from "./FactoryMethod.module.scss";
import { IFactoryMethodProps } from "./IFactoryMethodProps";
import {
IDetailsListItemState,
IDetailsNewsListItemState,
IDetailsDirectoryListItemState,
IDetailsAnnouncementListItemState,
IFactoryMethodState
} from "./IFactoryMethodState";
import { IListItem } from "./models/IListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { escape } from "#microsoft/sp-lodash-subset";
import { SPHttpClient, SPHttpClientResponse } from "#microsoft/sp-http";
import { ListItemFactory} from "./ListItemFactory";
import { TextField } from "office-ui-fabric-react/lib/TextField";
import {
DetailsList,
DetailsListLayoutMode,
Selection,
buildColumns,
IColumn
} from "office-ui-fabric-react/lib/DetailsList";
import { MarqueeSelection } from "office-ui-fabric-react/lib/MarqueeSelection";
import { autobind } from "office-ui-fabric-react/lib/Utilities";
import PropTypes from "prop-types";
//#endregion
export default class FactoryMethod extends React.Component<IFactoryMethodProps, IFactoryMethodState> {
constructor(props: IFactoryMethodProps, state: any) {
super(props);
this.setInitialState();
}
// lifecycle help here: https://staminaloops.github.io/undefinedisnotafunction/understanding-react/
//#region Mouting events lifecycle
// the data returned from render is neither a string nor a DOM node.
// it's a lightweight description of what the DOM should look like.
// inspects this.state and this.props and create the markup.
// when your data changes, the render method is called again.
// react diff the return value from the previous call to render with
// the new one, and generate a minimal set of changes to be applied to the DOM.
public render(): React.ReactElement<IFactoryMethodProps> {
switch(this.props.listName) {
case "GenericList":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsListItemState.items} columns={this.state.columns} />;
case "News":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsNewsListItemState.items} columns={this.state.columns}/>;
case "Announcements":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsAnnouncementListItemState.items} columns={this.state.columns}/>;
case "Directory":
// tslint:disable-next-line:max-line-length
return <this.ListMarqueeSelection items={this.state.DetailsDirectoryListItemState.items} columns={this.state.columns}/>;
default:
return null;
}
}
// invoked once, only on the client (not on the server), immediately AFTER the initial rendering occurs.
public componentDidMount(): void {
// you can access any refs to your children
// (e.g., to access the underlying DOM representation - ReactDOM.findDOMNode).
// the componentDidMount() method of child components is invoked before that of parent components.
// if you want to integrate with other JavaScript frameworks,
// set timers using setTimeout or setInterval,
// or send AJAX requests, perform those operations in this method.
this._configureWebPart = this._configureWebPart.bind(this);
this.readItemsAndSetStatus("");
}
//#endregion
//#region Props changes lifecycle events (after a property changes from parent component)
public componentWillReceiveProps(nextProps: IFactoryMethodProps): void {
if(nextProps.listName !== this.props.listName) {
this.readItemsAndSetStatus(nextProps.listName);
}
}
//#endregion
//#region private methods
private _configureWebPart(): void {
this.props.configureStartCallback();
}
public setInitialState(): void {
this.state = {
type: "ListItem",
status: this.listNotConfigured(this.props)
? "Please configure list in Web Part properties"
: "Ready",
columns:[],
DetailsListItemState:{
items:[]
},
DetailsNewsListItemState:{
items:[]
},
DetailsDirectoryListItemState:{
items:[]
},
DetailsAnnouncementListItemState:{
items:[]
},
};
}
// reusable inline component
private ListMarqueeSelection = (itemState: {columns: IColumn[], items: IListItem[] }) => (
<div>
<DetailsList
items={ itemState.items }
columns={ itemState.columns }
setKey="set"
layoutMode={ DetailsListLayoutMode.fixedColumns }
selectionPreservedOnEmptyClick={ true }
compact={ true }>
</DetailsList>
</div>
)
// read items using factory method pattern and sets state accordingly
private readItemsAndSetStatus(nextListName: string): void {
this.setState({
status: "Loading all items..."
});
const factory: ListItemFactory = new ListItemFactory();
factory.getItems(this.props.spHttpClient, this.props.siteUrl, nextListName)
.then((items: any[]) => {
const keyPart: string = this.props.listName === "GenericList" ? "" : nextListName;
// the explicit specification of the type argument `keyof {}` is bad and
// it should not be required.
this.setState<keyof {}>({
status: `Successfully loaded ${items.length} items`,
["Details" + keyPart + "ListItemState"] : {
items
},
columns: buildColumns(items)
});
});
}
private listNotConfigured(props: IFactoryMethodProps): boolean {
return props.listName === undefined ||
props.listName === null ||
props.listName.length === 0;
}
//#endregion
}
and my factory object
import { SPHttpClient, SPHttpClientResponse } from "#microsoft/sp-http";
import { IWebPartContext } from "#microsoft/sp-webpart-base";
import { IListItem} from "./models/IListItem";
import { IFactory } from "./IFactory";
import { INewsListItem } from "./models/INewsListItem";
import { IDirectoryListItem } from "./models/IDirectoryListItem";
import { IAnnouncementListItem } from "./models/IAnnouncementListItem";
export class ListItemFactory implements IFactory {
private _listItems: IListItem[];
public getItems(requester: SPHttpClient, siteUrl: string, listName: string): Promise<IListItem[]> {
if(listName === ""){
listName = "GenericList";
}
switch(listName) {
case "GenericList":
let items: IListItem[];
// tslint:disable-next-line:max-line-length
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,Author/Title,Editor/Title&$expand=Author,Editor`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IListItem[] }> => {
return response.json();
})
.then((json: { value: IListItem[] }) => {
console.log(JSON.stringify(json.value));
return items=json.value.map((v,i)=>(
{
//key: v.id,
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title
}
));
});
case "News":
let newsitems: INewsListItem[];
// tslint:disable-next-line:max-line-length
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,Created By,Modified By,newsheader,newsbody,expiryDate`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: INewsListItem[] }> => {
return response.json();
})
.then((json: { value: INewsListItem[] }) => {
return items=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
newsheader: v.newsheader,
newsbody: v.newsbody,
expiryDate: v.expiryDate
}
));
});
case "Announcements":
let announcementitems: IAnnouncementListItem[];
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IAnnouncementListItem[] }> => {
return response.json();
})
.then((json: { value: IAnnouncementListItem[] }) => {
return items=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
announcementBody: v.announcementBody,
expiryDate: v.expiryDate
}
));
});
case "Directory":
let directoryitems: IDirectoryListItem[];
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IDirectoryListItem[] }> => {
return response.json();
})
.then((json: { value: IDirectoryListItem[] }) => {
return items=json.value.map((v,i)=>(
{
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title,
firstName: v.firstName,
lastName: v.lastName,
mobileNumber: v.mobileNumber,
internalNumber: v.internalNumber
}
));
});
default:
// tslint:disable-next-line:max-line-length
return requester.get(`${siteUrl}/_api/web/lists/getbytitle('${listName}')/items?$select=Title,Id,Modified,Created,Author/Title,Editor/Title&$expand=Author,Editor`,
SPHttpClient.configurations.v1,
{
headers: {
"Accept": "application/json;odata=nometadata",
"odata-version": ""
}
})
.then((response: SPHttpClientResponse): Promise<{ value: IListItem[] }> => {
return response.json();
})
.then((json: { value: IListItem[] }) => {
console.log(JSON.stringify(json.value));
return items=json.value.map((v,i)=>(
{
//key: v.id,
id: v.Id,
title: v.Title,
created: v.Created,
createdby: v.Author.Title,
modified: v.Modified,
modifiedby: v.Editor.Title
}
));
});
}
}
}
The problem is that the first time it renders, the listname is undefined, so it reads data from the GenericList and I can see the grid rendered which is desired behavior
But whenever I select another list, I get an empty string as parameter on the getlistitems parameter, its not taking either Announcements, News, etc
What am I missing here?
Please Note: I do not use the SharePoint framework.
I believe your issue lies in the integration between the SharePoint framework WebPart and React. You don't see a change in the React app after the dropdown selection because the React app never receives a change in its properties.
There are two options:
1) Manually call FactoryMethodWebPart.render() when the dropdown changes during the setSelectedList(). Since I do not use the SharePoint framework, I do not know if this is best practice. I do know that this would re-build the React app and insert it into the DOM again instead of triggering a re-render.
2) Use a higher order component that allows you to pass new props to the already built React app. This solution is based on this article. Idea is to build a function that returns a wrapper around a Component. This function accepts both the Component to render and a subscribe function that in turn accepts the function that can be used to pass new props to the component. When the Component is created, it calls the subscribe() method, and simply renders the Component passed to it.
The HOC:
const connect = (Component, subscribe) => class extends React.Component {
constructor(props) {
super(props);
const rerender = props => this.setState(props);
subscribe(rerender);
}
render() {
const props = { ...this.props, ...this.state };
return <Component {...props} />;
}
}
The use case:
export default class FactoryMethodWebPart extends BaseClientSideWebPart<IFactoryMethodWebPartProps> {
// other private fields
private _factoryMethodComponent;
private _setFactoryMethodProps;
protected onInit(): Promise<void> {
// ...
this._factoryMethodComponent = connect(FactoryMethod, rerender => this._setFactoryMethodProps = rerender);
}
public render(): void {
const element = React.createElement(
this._factoryMethodComponent, // use the connected component instead of FactoryMethod
{ ... }
] );
this._factorymethodContainerComponent = ReactDom.render(element, this.domElement);
}
private setSelectedList(value: string): void {
// ...
this._setFactoryMethodProps({ listName: selectedDropDownOption ? selectedDropDownOption.text : 'GenericList' });
}
}

React Redux Reducer: 'this.props.tasks.map is not a function' error

I am making a React Redux example; however, I ran into an issue and get the error below:
TypeError: this.props.tasks.map is not a function
[Learn More]
I have tried many things and I cannot seem to understand why this is not working. I believe it is when the allReducers maps the tasks from the Tasks function. I have fixed this error back and forth but then it would complain it was undefined. I would fix that and loop back to this issue. Any help would be appreciated. Im sure I am making a simple mistake. Below are my following files
App.js
import React from 'react';
import TaskBoard from "../containers/task-board";
require('../../scss/style.scss');
const App = () => (
<div>
<h2>Task List</h2>
<hr />
<TaskBoard/>
</div>
);
export default App;
index.js
import {combineReducers} from 'redux';
import {Tasks} from './reducer-tasks';
const allReducers = combineReducers({
tasks: Tasks
});
export default allReducers
task-board.js
import React, {Component} from 'react';
import {bindActionCreators} from 'redux';
import {connect} from 'react-redux';
import {deleteTaskAction} from '../actions/ActionIndex';
import {editTaskAction} from '../actions/ActionIndex';
class TaskBoard extends Component {
renderList() {
return this.props.tasks.map((task) => {
if(task.status == "pending"){
return (<li key={task.id}>
{task.id} {task.description}
<button type="button">Finish</button>
<button type="button">Edit</button>
<button onClick={() => this.props.deleteTask(task)} type="button">Delete</button>
</li>
);
}
});
}
render() {
if (!this.props.tasks) {
console.log(this.props.tasks);
return (<div>You currently have no tasks, please first create one...</div>);
}
return (
<div>
{this.renderList()}
</div>
);
}
}
function mapStateToProps(state) {
return {
tasks: state.tasks
};
}
function matchDispatchToProps(dispatch){
return bindActionCreators(
{
deleteTask: deleteTaskAction,
editTask: editTaskAction
}, dispatch)
}
export default connect(mapStateToProps,matchDispatchToProps)(TaskBoard);
reducer-tasks.js
const initialState = {
tasks: [
{
id: 1,
description: "This is a task",
status: "pending"
},
{
id: 2,
description: "This is another task",
status: "pending"
},
{
id: 3,
description: "This is an easy task",
status: "pending"
}
]
}
export function Tasks (state = initialState, action) {
switch (action.type) {
case 'ADD_TASK':
return Object.assign({}, state, {
tasks: [
...state.tasks,
{
description: action.text,
status: action.status
}
]
})
break;
case 'EDIT_TASK':
return action.payload;
break;
case 'DELETE_TASK':
return Object.assign({}, state, {
status: action.status
})
break;
}
return state;
}
actionindex.js
export const addTaskAction = (task) => {
return {
type: 'ADD_TASK',
text: "Here is a sample description",
status: "pending"
}
};
export const deleteTaskAction = (task) => {
return {
type: 'DELETE_TASK',
status: "deleted"
}
};
export const editTaskAction = (task) => {
return {
type: 'EDIT_TASK',
payload: task
}
};
It's because the function 'map' can only be used for arrays, not for objects.
If you print out this.props.tasks in the render function of task-board.js you'll see that it's an OBJECT which contains the tasks array, not the actual tasks array itself.
So to fix this it's quite easy, instead of:
return this.props.tasks.map((task) => {
it's
return this.props.tasks.tasks.map((task) => {
Then it works
According to your reducer composition, your initialState should be:
const initialState = [
{
id: 1,
description: "This is a task",
status: "pending"
},
{
id: 2,
description: "This is another task",
status: "pending"
},
{
id: 3,
description: "This is an easy task",
status: "pending"
}
]
Check server code if you're trying to render before getting the data. If that's the case, redux/react throws .map is not a function.
Always get the data, use middleware and then try to render client from server.
I faced the same in redux and the problem was I was trying to fetch data after rendering the client

How can I compose query fragments with variables

I am using Relay and react-router-relay and am trying to compose several Relay containers. The inner container needs a query fragment variable that must be passed from the router down through its parent containers. It's not getting this variable.
Here's how the code looks:
// A "viewer":
const UserQueries = { user: () => Relay.QL`query { user }` };
// A route to compose a message:
<Route
path='/compose/:id'
component={ ComposeContainer }
queries={ UserQueries }
/>
// A container:
const ComposeContainer = Relay.createContainer(
Compose,
{
initialVariables: {
id: null
},
fragments: {
user: () => Relay.QL`
fragment on User {
${ BodyContainer.getFragment('user') }
// other fragments
}
`
}
}
);
// And the composed container:
const BodyContainer = React.createContainer(
Body,
{
initialVariables: {
id: null
},
fragments: {
user: () => Relay.QL`
fragment on User {
draft(id: $id) {
// fields
}
}
`
}
}
);
The draft field inside BodyContainer never gets $id from the route param. The signature to RelayContainer.getFragment() has arguments that seem to let you pass params and variables, but I am not sure how this should be used.
Your user fragment on <ComposeContainer> has to be something like:
user: ({ id }) => Relay.QL`
fragment on User {
${BodyContainer.getFragment('user', { id })}
// other fragments
}
`
Additionally, when you compose in <BodyContainer> from <ComposeContainer>. You'll also need to pass in id as a prop, e.g.
<BodyContainer id={this.props.id} />
See also additional discussion at https://github.com/facebook/relay/issues/309.

Categories

Resources