react-admin how to create client side list controller - react-admin
Do you know how to create client-side list controller?
I checked StackOverflow - I didn't find the answer to my question.
Is there a simple way to create a resource which reads all records from server on first page load.
And then the sort, filter, paging is done client side.
Also to have option to disable paging.
I tried to make a copy of ListController.js from ra-core and List.js from ra-ui-materialui and customize it, but error and I could go forward. I am new to react and making PoC.
Here is the error:
TypeError: Cannot read property 'apply' of undefined
(anonymous function)
node_modules/recompose/compose.js:22
19 |
20 | return funcs.reduce(function (a, b) {
21 | return function () {
22 | return a(b.apply(undefined, arguments));
23 | };
24 | });
25 | }
View compiled
./src/ListController.js
src/ListController.js:431
428 | export default compose(
429 | connect(
430 | mapStateToProps,
431 | {
432 | crudGetList: crudGetListAction,
433 | changeListParams: changeListParamsAction,
434 | setSelectedIds: setListSelectedIdsAction,
View compiled
I have changed the imports from relative paths to import react-admin;
Any suggestions on how to fix the error?
This is the changed ListController.js file:
/* eslint no-console: ["error", { allow: ["warn", "error"] }] */
import { Component } from 'react';
import PropTypes from 'prop-types';
import { connect } from 'react-redux';
import { parse, stringify } from 'query-string';
import { push as pushAction } from 'react-router-redux';
import compose from 'recompose/compose';
import { createSelector } from 'reselect';
import inflection from 'inflection';
import debounce from 'lodash/debounce';
import isEqual from 'lodash/isEqual';
import pickBy from 'lodash/pickBy';
import removeEmpty from 'react-admin';
import queryReducer, {
SET_SORT,
SET_PAGE,
SET_PER_PAGE,
SET_FILTER,
SORT_DESC,
} from 'react-admin';
import { crudGetList as crudGetListAction } from 'react-admin';
import {
changeListParams as changeListParamsAction,
setListSelectedIds as setListSelectedIdsAction,
toggleListItem as toggleListItemAction,
} from 'react-admin';
import translate from 'react-admin';
import removeKey from 'react-admin';
/**
* List page component
*
* The <List> component renders the list layout (title, buttons, filters, pagination),
* and fetches the list of records from the REST API.
* It then delegates the rendering of the list of records to its child component.
* Usually, it's a <Datagrid>, responsible for displaying a table with one row for each post.
*
* In Redux terms, <List> is a connected component, and <Datagrid> is a dumb component.
*
* Props:
* - title
* - perPage
* - sort
* - filter (the permanent filter to apply to the query)
* - actions
* - filters (a React Element used to display the filter form)
* - pagination
*
* #example
* const PostFilter = (props) => (
* <Filter {...props}>
* <TextInput label="Search" source="q" alwaysOn />
* <TextInput label="Title" source="title" />
* </Filter>
* );
* export const PostList = (props) => (
* <List {...props}
* title="List of posts"
* sort={{ field: 'published_at' }}
* filter={{ is_published: true }}
* filters={<PostFilter />}
* >
* <Datagrid>
* <TextField source="id" />
* <TextField source="title" />
* <EditButton />
* </Datagrid>
* </List>
* );
*/
export class ListController extends Component {
state = {};
componentDidMount() {
if (
!this.props.query.page &&
!(this.props.ids || []).length &&
this.props.params.page > 1 &&
this.props.total > 0
) {
this.setPage(this.props.params.page - 1);
return;
}
this.updateData();
if (Object.keys(this.props.query).length > 0) {
this.props.changeListParams(this.props.resource, this.props.query);
}
}
componentWillUnmount() {
this.setFilters.cancel();
}
componentWillReceiveProps(nextProps) {
if (
nextProps.resource !== this.props.resource ||
nextProps.query.sort !== this.props.query.sort ||
nextProps.query.order !== this.props.query.order ||
nextProps.query.page !== this.props.query.page ||
nextProps.query.filter !== this.props.query.filter
) {
this.updateData(
Object.keys(nextProps.query).length > 0
? nextProps.query
: nextProps.params
);
}
if (nextProps.version !== this.props.version) {
this.updateData();
}
}
shouldComponentUpdate(nextProps, nextState) {
if (
nextProps.translate === this.props.translate &&
nextProps.isLoading === this.props.isLoading &&
nextProps.version === this.props.version &&
nextState === this.state &&
nextProps.data === this.props.data &&
nextProps.selectedIds === this.props.selectedIds &&
nextProps.total === this.props.total
) {
return false;
}
return true;
}
/**
* Merge list params from 4 different sources:
* - the query string
* - the params stored in the state (from previous navigation)
* - the filter defaultValues
* - the props passed to the List component
*/
getQuery() {
const query =
Object.keys(this.props.query).length > 0
? this.props.query
: { ...this.props.params };
const filterDefaultValues = this.props.filterDefaultValues || {};
query.filter = { ...filterDefaultValues, ...query.filter };
if (!query.sort) {
query.sort = this.props.sort.field;
query.order = this.props.sort.order;
}
if (!query.perPage) {
query.perPage = this.props.perPage;
}
if (!query.page) {
query.page = 1;
}
return query;
}
updateData(query) {
const params = query || this.getQuery();
const { sort, order, page = 1, perPage, filter } = params;
const pagination = {
page: parseInt(page, 10),
perPage: parseInt(perPage, 10),
};
const permanentFilter = this.props.filter;
this.props.crudGetList(
this.props.resource,
pagination,
{ field: sort, order },
{ ...filter, ...permanentFilter }
);
}
setSort = sort => this.changeParams({ type: SET_SORT, payload: sort });
setPage = page => this.changeParams({ type: SET_PAGE, payload: page });
setPerPage = perPage =>
this.changeParams({ type: SET_PER_PAGE, payload: perPage });
setFilters = debounce(filters => {
if (isEqual(filters, this.props.filterValues)) {
return;
}
// fix for redux-form bug with onChange and enableReinitialize
const filtersWithoutEmpty = removeEmpty(filters);
this.changeParams({ type: SET_FILTER, payload: filtersWithoutEmpty });
}, this.props.debounce);
showFilter = (filterName, defaultValue) => {
this.setState({ [filterName]: true });
if (typeof defaultValue !== 'undefined') {
this.setFilters({
...this.props.filterValues,
[filterName]: defaultValue,
});
}
};
hideFilter = filterName => {
this.setState({ [filterName]: false });
const newFilters = removeKey(this.props.filterValues, filterName);
this.setFilters(newFilters);
};
handleSelect = ids => {
this.props.setSelectedIds(this.props.resource, ids);
};
handleUnselectItems = () => {
this.props.setSelectedIds(this.props.resource, []);
};
handleToggleItem = id => {
this.props.toggleItem(this.props.resource, id);
};
changeParams(action) {
const newParams = queryReducer(this.getQuery(), action);
this.props.push({
...this.props.location,
search: `?${stringify({
...newParams,
filter: JSON.stringify(newParams.filter),
})}`,
});
this.props.changeListParams(this.props.resource, newParams);
}
render() {
const {
basePath,
children,
resource,
hasCreate,
data,
ids,
total,
isLoading,
translate,
version,
selectedIds,
} = this.props;
const query = this.getQuery();
const queryFilterValues = query.filter || {};
const resourceName = translate(`resources.${resource}.name`, {
smart_count: 2,
_: inflection.humanize(inflection.pluralize(resource)),
});
const defaultTitle = translate('ra.page.list', {
name: `${resourceName}`,
});
return children({
basePath,
currentSort: {
field: query.sort,
order: query.order,
},
data,
defaultTitle,
displayedFilters: this.state,
filterValues: queryFilterValues,
hasCreate,
hideFilter: this.hideFilter,
ids,
isLoading,
onSelect: this.handleSelect,
onToggleItem: this.handleToggleItem,
onUnselectItems: this.handleUnselectItems,
page: parseInt(query.page || 1, 10),
perPage: parseInt(query.perPage, 10),
refresh: this.refresh,
resource,
selectedIds,
setFilters: this.setFilters,
setPage: this.setPage,
setPerPage: this.setPerPage,
setSort: this.setSort,
showFilter: this.showFilter,
translate,
total,
version,
});
}
}
ListController.propTypes = {
// the props you can change
children: PropTypes.func.isRequired,
filter: PropTypes.object,
filters: PropTypes.element,
filterDefaultValues: PropTypes.object, // eslint-disable-line react/forbid-prop-types
pagination: PropTypes.element,
perPage: PropTypes.number.isRequired,
sort: PropTypes.shape({
field: PropTypes.string,
order: PropTypes.string,
}),
// the props managed by react-admin
authProvider: PropTypes.func,
basePath: PropTypes.string.isRequired,
changeListParams: PropTypes.func.isRequired,
crudGetList: PropTypes.func.isRequired,
data: PropTypes.object, // eslint-disable-line react/forbid-prop-types
debounce: PropTypes.number,
filterValues: PropTypes.object, // eslint-disable-line react/forbid-prop-types
hasCreate: PropTypes.bool.isRequired,
hasEdit: PropTypes.bool.isRequired,
hasList: PropTypes.bool.isRequired,
hasShow: PropTypes.bool.isRequired,
ids: PropTypes.array,
selectedIds: PropTypes.array,
isLoading: PropTypes.bool.isRequired,
location: PropTypes.object.isRequired,
path: PropTypes.string,
params: PropTypes.object.isRequired,
push: PropTypes.func.isRequired,
query: PropTypes.object.isRequired,
resource: PropTypes.string.isRequired,
setSelectedIds: PropTypes.func.isRequired,
toggleItem: PropTypes.func.isRequired,
total: PropTypes.number.isRequired,
translate: PropTypes.func.isRequired,
version: PropTypes.number,
};
ListController.defaultProps = {
debounce: 500,
filter: {},
filterValues: {},
perPage: 10,
sort: {
field: 'id',
order: SORT_DESC,
},
};
const injectedProps = [
'basePath',
'currentSort',
'data',
'defaultTitle',
'displayedFilters',
'filterValues',
'hasCreate',
'hideFilter',
'ids',
'isLoading',
'onSelect',
'onToggleItem',
'onUnselectItems',
'page',
'perPage',
'refresh',
'resource',
'selectedIds',
'setFilters',
'setPage',
'setPerPage',
'setSort',
'showFilter',
'total',
'translate',
'version',
];
/**
* Select the props injected by the ListController
* to be passed to the List children need
* This is an implementation of pick()
*/
export const getListControllerProps = props =>
injectedProps.reduce((acc, key) => ({ ...acc, [key]: props[key] }), {});
/**
* Select the props not injected by the ListController
* to be used inside the List children to sanitize props injected by List
* This is an implementation of omit()
*/
export const sanitizeListRestProps = props =>
Object.keys(props)
.filter(props => !injectedProps.includes(props))
.reduce((acc, key) => ({ ...acc, [key]: props[key] }), {});
const validQueryParams = ['page', 'perPage', 'sort', 'order', 'filter'];
const getLocationPath = props => props.location.pathname;
const getLocationSearch = props => props.location.search;
const selectQuery = createSelector(
getLocationPath,
getLocationSearch,
(path, search) => {
const query = pickBy(
parse(search),
(v, k) => validQueryParams.indexOf(k) !== -1
);
if (query.filter && typeof query.filter === 'string') {
try {
query.filter = JSON.parse(query.filter);
} catch (err) {
delete query.filter;
}
}
return query;
}
);
function mapStateToProps(state, props) {
const resourceState = state.admin.resources[props.resource];
return {
query: selectQuery(props),
params: resourceState.list.params,
ids: resourceState.list.ids,
selectedIds: resourceState.list.selectedIds,
total: resourceState.list.total,
data: resourceState.data,
isLoading: state.admin.loading > 0,
filterValues: resourceState.list.params.filter,
version: state.admin.ui.viewVersion,
};
}
export default compose(
connect(
mapStateToProps,
{
crudGetList: crudGetListAction,
changeListParams: changeListParamsAction,
setSelectedIds: setListSelectedIdsAction,
toggleItem: toggleListItemAction,
push: pushAction,
}
),
translate
)(ListController);
It would be easier to handle this use case with a custom dataProvider.
Each time you are executing an action in a List view (sorting, filtering, etc), React Admin asks the data from the data provider.
For example, say we want to list users, then filter them by gender, then sort them by age. The data provider will be called three times with the following arguments:
dataProvider('GET_LIST', 'user', { sort: defaultSort, filters: defaultFilters });
dataProvider('GET_LIST', 'user', { sort: defaultSort, filters: { gender: 'm' } });
dataProvider('GET_LIST', 'user', { sort: { field: 'age', order: 'DESC' }, filters: { gender: 'm' } });
So, in order to implement the behavior you want, your custom data provider would be something like:
// customDataResolver.js
import simpleRestProvider from 'ra-data-simple-rest';
const restDataProvider = simpleRestDataProvider('http://example.com/api');
let users; // In-memory cache (but you can write in storage or something)
export default (type, resource, params) => {
if (type === 'GET_LIST' && resource === 'users') {
if (!users) {
return restDataProvider(type, resource, params)
.then((data) => {
users = data;
return data;
});
}
return users
.sort(sortBy(params.sort))
.filter(filterBy(params.filter));
};
return restDataProvider(type, resource, params);
};
Related
Awaiting asynchronous params when using xstate `useInterpret`
I want to enable persistance for react-native application. Following tutorial on https://garden.bradwoods.io/notes/javascript/state-management/xstate/global-state#rehydratestate I can't use asynchronous code inside xstate's hook useInterpret Original code (which uses localStorage instead of AsyncStorage) doesn't have that issue since localStorage is synchronous. import AsyncStorage from '#react-native-async-storage/async-storage'; import { createMachine } from 'xstate'; import { createContext } from 'react'; import { InterpreterFrom } from 'xstate'; import { useInterpret } from '#xstate/react'; export const promiseMachine = createMachine({ id: 'promise', initial: 'pending', states: { pending: { on: { RESOLVE: { target: 'resolved' }, REJECT: { target: 'rejected' }, }, }, resolved: {}, rejected: {}, }, tsTypes: {} as import('./useGlobalMachine.typegen').Typegen0, schema: { events: {} as { type: 'RESOLVE' } | { type: 'REJECT' }, }, predictableActionArguments: true, }); export const GlobalStateContext = createContext({ promiseService: {} as InterpreterFrom<typeof promiseMachine>, }); const PERSISTANCE_KEY = 'test_key'; export const GlobalStateProvider = (props) => { const rehydrateState = async () => { return ( JSON.parse(await AsyncStorage.getItem(PERSISTANCE_KEY)) || (promiseMachine.initialState as unknown as typeof promiseMachine) ); }; const promiseService = useInterpret( promiseMachine, { state: await rehydrateState(), // ERROR: 'await' expressions are only allowed within async functions and at the top levels of modules. }, (state) => AsyncStorage.setItem(PERSISTANCE_KEY, JSON.stringify(state)) ); return ( <GlobalStateContext.Provider value={{ promiseService }}> {props.children} </GlobalStateContext.Provider> ); }; I tried to use .then syntax to initialize after execution of async function but it caused issue with conditional rendering of hooks.
I had the same use case recently and from what I found there is no native way for xState to handle the async request. What is usually recommended is to introduce a generic wrapper component that takes the state from the AsyncStorage and pass it a prop to where it is needed. In your App.tsx you can do something like: const [promiseMachineState, setPromiseMachineState] = useState<string | null>(null); useEffect(() => { async function getPromiseMachineState() { const state = await AsyncStorage.getItem("test_key"); setPromiseMachineState(state); } getAppMachineState(); }, []); return ( promiseMachineState && ( <AppProvider promiseMachineState={promiseMachineState}> ... </AppProvider> ) ) And then in your global context you can just consume the passed state: export const GlobalStateProvider = (props) => { const promiseService = useInterpret( promiseMachine, { state: JSON.parse(props.promiseMachineState) }, (state) => AsyncStorage.setItem(PERSISTANCE_KEY, JSON.stringify(state)) ); return ( <GlobalStateContext.Provider value={{ promiseService }}> {props.children} </GlobalStateContext.Provider> ); };
How can I update my items in redux state?
I have state that looks following: const initialState = { employee: '', companyNumber: '', insuranceCompany: '', workHealthcare: '', actionGuide: '', } And I have screen where I want to update/edit these values. And the updated values are shown here in this screen. this is my action file: const UPDATE_EMPLOYEE_DETAILS = 'UPDATE_EMPLOYEE_DETAILS' const UPDATE_COMPANYNUMBER_DETAILS = 'UPDATE_COMPANYNUMBER_DETAILS' const UPDATE_INSURANCECOMPANY_DETAILS = 'UPDATE_INSURANCECOMPANY_DETAILS' const UPDATE_WORKHEALTHCARE_DETAILS = 'UPDATE_WORKHEALTHCARE_DETAILS' const UPDATE_ACTIONGUIDE_DETAILS = 'UPDATE_ACTIONGUIDE_DETAILS' export const employeeEditAction = (text) => ({ type: UPDATE_EMPLOYEE_DETAILS, payload: { employee: text }, }) export const companyNumberEditAction = (text) => ({ type: UPDATE_COMPANYNUMBER_DETAILS, payload: { companyNumber: text }, }) export const insuranceCompanyEditAction = (text) => ({ type: UPDATE_INSURANCECOMPANY_DETAILS, payload: { insuranceCompany: text }, }) export const workHealthcareEditAction = (text) => ({ type: UPDATE_WORKHEALTHCARE_DETAILS, payload: { workHealthcare: text }, }) export const actionGuideEditAction = (text) => ({ type: UPDATE_ACTIONGUIDE_DETAILS, payload: { actionGuide: text }, }) and this is the reducer file. const UPDATE_WORKPLACE_DETAILS = 'UPDATE_WORKPLACE_DETAILS' const initialState = { employee: '', companyNumber: '', insuranceCompany: '', workHealthcare: '', actionGuide: '', } const workplaceValueReducer = (state = initialState, action) => { switch (action.type) { case UPDATE_WORKPLACE_DETAILS: return { ...state, employee: action.payload.employee, companyNumber: action.payload.companyNumber, insuranceCompany: action.payload.insuranceCompany, workHealthcare: action.payload.workHealthcare, actionGuide: action.payload.actionGuide } default: return state } } export default workplaceValueReducer This is the screen and the function that should save the edited values. const saveWorkPlaceDetails = () => { if (employee.length > 0) { // props.editWorkplaceDetails( // employee, // companyNumber, // insuranceCompany, // workHealthcare, // info, // ) props.saveEmployee(employee) props.saveCompanyNumber(companyNumber) props.saveInsuranceCompany(insuranceCompany) props.saveWorkHealthcare(workHealthcare) props.saveActionGuide(info) setEmployee('') setCompanyNumber('') setInsuranceCompany('') setWorkHealthcare('') setInfo('') workDetailsSavedToast() } else { workDetailsErrorToast() } } const mapDispatchToProps = (dispatch) => ({ //editWorkplaceDetails: bindActionCreators(, dispatch), saveEmployee: (text) => dispatch(employeeEditAction(text)), saveCompanyNumber: (text) => dispatch(companyNumberEditAction(text)), saveInsuranceCompany: (text) => dispatch(insuranceCompanyEditAction(text)), saveWorkHealthcare: (text) => dispatch(workHealthcareEditAction(text)), saveActionGuide: (text) => dispatch(actionGuideEditAction(text)), }) export default connect(null, mapDispatchToProps)(EditWorkplaceDetails) The input fields looks as following: <Input style={inputLicenseEdit} inputContainerStyle={{ borderBottomColor: colors.white, width: '100%', }} placeholder='Työnantaja' placeholderTextColor={colors.white} leftIcon={ <Ionicons name='ios-person-outline' size={30} color={colors.white} /> } onChangeText={(text) => { setEmployee(text) }} />[![enter image description here][1]][1] Currently when I try to update the items in the state it doesn't update and triggers the error toast instead. What do I need to change here?
You should set as many case in your reducer switch that you have actions' consts. Example for one action : export const UPDATE_EMPLOYEE_DETAILS = 'UPDATE_EMPLOYEE_DETAILS' export const employeeEditAction = (employee) => ({ type: UPDATE_EMPLOYEE_DETAILS, employee, }) import {UPDATE_EMPLOYEE_DETAILS, /** ... other actions */} from 'action path'; const workplaceValueReducer = (state = initialState, action) => { switch (action.type) { case UPDATE_EMPLOYEE_DETAILS: return { ...state, employee: action.employee } // ... other cases default: return state } } It looks like you update all your workplace's values at the same time, so you should simply do : export const UPDATE_WORKPLACE_DETAILS = 'UPDATE_WORKPLACE_DETAILS' export const updateWorkPlaceDetailsAction = (details) => ({ type: UPDATE_WORKPLACE_DETAILS, details, }) import {UPDATE_WORKPLACE_DETAILS} from 'action path'; const workplaceValueReducer = (state = initialState, action) => { switch (action.type) { case UPDATE_WORKPLACE_DETAILS: return action.details || state; default: return state } } I personnaly set both cases, so one action for the whole object, plus one action foreach object's attributes I want to update PS : write "js" after your first line backticks block to get code colors
"What if I want to have the cars listed under certain user like this. user: {cars: [{}]}" (Replied in previous answered) It depends of what you want to do. Examples : // state is your user const userReducer = (state = initialState, action) => { switch (action.type) { case ADD_CAR: const nextState = { ...state, cars: state.cars.slice() // copy array, because all children that ara objects and arrays, should be immutable too }; return nextState; newtState.cars.push(action.car); /** can be done like that too, but it consume more performance */ return { ...state, cars: [...state.cars, action.car] } case UPDATE_CAR: const nextState = { ...state, cars: state.cars.slice(); }; nextState.cars[action.index] = action.car; /** OR even : */ nextState.cars[action.index] = {...state.cars[action.index], [action.key]: action.value} return nextState; case REMOVE_CAR: const nextState = { ...state, cars: state.cars.slice() }; // do not use splice before copying the array and do not return directly splice newtState.cars.splice(action.index, 1); return nextState; default: return state } } see documentation for slice and splice
How to use 'await/async' in react native?
I am getting isValidate value from sqlite and routing user to MainTabs or ValidateUser components. But I can not do it because it is not working async/await (Or I couldn't.). initialRouteName will be assigned the name of the component to which users will be routed. Now react native routing to "ValidateUser". Not: When I try flag async the MainNavigationContainer and I write await directly inside it is throwing error. Not: I am using expo-sqlite-orm for getting data from database https://github.com/dflourusso/expo-sqlite-orm //imports truncated... const Stack = createStackNavigator() //truncated... const MainNavigationContainer = (props) => { console.log("MainNavigationContainer props", props) var initialRouteName = "ValidateUser" async function validateUserFunc () { const validatedUser = await Users.findBy({isAdmin_eq: 1}) console.log('validatedUser in validateUserFunc: ', validatedUser) props.setValidatedUser(validatedUser) let userIsValidated = validatedUser.isValidated let validatedTelNumber = validatedUser.telNo initialRouteName = userIsValidated === 1 ? "MainTabs" : "ValidateUser" console.log('initialRouteName in validateUserFunc: ', initialRouteName) } validateUserFunc() console.log('initialRouteName after validateUserFunc: ', initialRouteName) //truncated... return ( <NavigationContainer> <Stack.Navigator screenOptions={screenOptions} initialRouteName={initialRouteName} > //truncated Stack.Screen... </Stack.Navigator> </NavigationContainer> ) } const mapStateToProps = state => { return { ...state } } export default connect(mapStateToProps, {validateUser, listenMessages, setValidatedUser})(MainNavigationContainer) Users.js entity: import * as SQLite from 'expo-sqlite' import { BaseModel, types } from 'expo-sqlite-orm' import * as FileSystem from "expo-file-system"; export default class Users extends BaseModel { constructor(obj) { super(obj) } static get database() { /*return async () => SQLite.openDatabase({ name:"ulak.db", location:"default" })*/ return async () => SQLite.openDatabase("ulak.db") //return async () => SQLite.openDatabase(`${FileSystem.documentDirectory}SQLite/ulak.db`) } static get tableName() { return 'users' } static get columnMapping() { return { id: { type: types.INTEGER, primary_key: true }, // For while only supports id as primary key userName: { type: types.TEXT, not_null: true }, userSurname: { type: types.TEXT, not_null: true }, telNo: { type: types.TEXT, not_null: true }, deviceId: { type: types.TEXT }, isValidated: { type: types.INTEGER, default: 0, not_null: true }, isAdmin: { type: types.INTEGER, not_null: false, default: 0 },//if isAdmin=1 phone owner, it will authentication. profilePicture: { type: types.TEXT, default: null }, registerDate: { type: types.DATETIME, not_null: true, default: () => Date.now() } } } }
can't set response from api to messages array of GiftedChat
I am new to react native. I am currently developing a messaging app. I have used npm-giftedChat for UI & functionalities. The problem is I need to get the response from api & set it to the messages array of giftedchat. I receive data from API and while I set it to messages array it loops over data and renders only the last data in that array. Any help would be appreciated.I have added my code here Please find where I am going wrong? componentWillMount() { var arrMsg = []; var data = params.data for(let i = 0; i < data.Replies.length ; i++){ var obj = { _id: data.To._id, text: data.Replies[i].Reply, createdAt: data.Replies[i].CreatedDate, user: { _id: data.From._id, name: 'React Native', avatar: data.From.Profile.DisplayPicture }, image: '', } arrMsg.push(obj) } this.setState({messages: arrMsg}) } Sample output
My self also facing same issues.. setting is very important in gifted chat.. so try to use following in ur code,i have edited same like your code.if any queries let me know thanks. for (let i = 0; i < data.Replies.length; i++) { console.log(data.Replies[i].CreatedDate); debugger var id = data.From._id if (data.To.id == UserID) { id = this.state.userID } const obj = { _id: Math.round(Math.random() * 1000000), text: data.Replies[i].Reply, createdAt: data.Replies[i].CreatedDate, user: { _id: id, name: 'React Native', avatar: data.From.Profile.DisplayPicture }, image: '', } arrMsg.push(obj); }; this.setState((previousState) => { return { messages: GiftedChat.append(previousState.messages, arrMsg) }; });
I wrote a gist here on how to add a web socket listening to a rails channel to a react native chat screen + Gifted Chat // chat.js import React, { Component } from 'react'; import { Text, View, StyleSheet, TouchableHighlight, Dimensions, AppState, AsyncStorage, Alert } from 'react-native'; import { GiftedChat, Actions, Bubble, SystemMessage } from 'react-native-gifted-chat'; import axios from 'axios'; import ActionCable from 'react-native-actioncable'; import { yourRootUrl, websocketUrl } from '../config/constants'; class Chat extends Component { state = { messages: [], client: '', accessToken: '', expiry: '', uid: '', userId: '' } componentDidMount() { AsyncStorage.multiGet( ['client', 'expiry', 'access_token', 'uid', 'account_balance', 'userId' ] ) .then((result) => { this.setState({ client: result[0][1], expiry: result[1][1], accessToken: result[2][1], uid: result[3][1], userId: result[5][1] }); }) .then(() => { this.getPreviousMessages(); }) .then(() => { this.createSocket(); }) .catch(() => { //error logic }); } getPreviousMessages() { //when we open the chat page we should load previous messages const { chatId } = this.props.navigation.state.params; const { client, accessToken, uid, userId } = this.state; const url = yourRootUrl + '/chats/' + chatId; const headers = { 'access-token': accessToken, client, expiry, uid }; axios.get(url, { headers }) .then((response) => { /* lets construct our messages to be in same format as expected by GiftedChat */ const allMessages = []; response.data.included.forEach((x) => { if (x.attributes.system) { const sysMessage = { _id: x.id, text: x.attributes['message-text'], createdAt: new Date(x.attributes['created-at']), system: true }; allMessages.push(sysMessage); } else { const userMessage = { _id: x.id, text: x.attributes['message-text'], createdAt: new Date(x.attributes['created-at']), user: { _id: x.attributes['sender-id'], avatar: x.attributes['sender-avatar'], }, image: x.attributes.image, }; allMessages.push(userMessage); } }); if (allMessages.length === response.data.included.length) { //lets sort messages according to date created const sortAllMessages = allMessages.sort((a, b) => b.createdAt - a.createdAt ); this.setState((previousState) => { return { messages: GiftedChat.append(previousState.messages, sortAllMessages) }; }); } }) } createSocket() { //assuming you have set up your chatchannel in your rails backend const { client, accessToken, uid, userId } = this.state; const { chatId } = this.props.navigation.state.params; //using react-navigation const WEBSOCKET_HOST = websocketUrl + 'access-token=' + accessToken + '&client=' + client + '&uid=' + uid; const cable = ActionCable.createConsumer(WEBSOCKET_HOST); this.channel = cable.subscriptions.create( { channel: 'ChatChannel', id: chatId }, { received: (data) => { console.log('Received Data:', data); if ((data.message.sender_id !== parseInt(userId)) || (data.message.image !== null)) { //ensuring you do not pick up your own messages if (data.message.system === true) { const sysMessage = { _id: data.message.id, text: data.message.message_text, createdAt: new Date(data.message.created_at), system: true }; this.setState((previousState) => { return { messages: GiftedChat.append(previousState.messages, sysMessage) }; }); } else { const userMessage = { _id: data.message.id, text: data.message.message_text, createdAt: new Date(data.message.created_at), user: { _id: data.message.sender_id, avatar: data.message.sender_avatar, }, image: data.message.image, }; this.setState((previousState) => { return { messages: GiftedChat.append(previousState.messages, userMessage) }; }); } } }, connected: () => { console.log(`Connected ${chatId}`); }, disconnected: () => { console.warn(`${chatId} was disconnected.`); }, rejected: () => { console.warn('connection rejected'); }, }); } onSend(messages = []) { const { chatId } = this.props.navigation.state.params; const { client, accessToken, uid, userId } = this.state; this.setState((previousState) => { return { messages: GiftedChat.append(previousState.messages, messages) }; }); messages.forEach((x) => { const url = yourRootUrl + '/messages'; const headers = { 'access-token': accessToken, client, expiry, uid }; const data = { chat_id: chatId, sender_id: userId, sender_name: name, message_text: x.text, image: x.image }; /* send the message to your rails app backend hopefully you have a callback in your model like after_create :broadcast_message then broadcast to the chat channel from your rails backend */ axios.post(url, data, { headers }) .then(response => console.log(response)); }); } renderBubble(props) { return ( <Bubble {...props} wrapperStyle={{ left: { backgroundColor: '#f9f9f9', } }} /> ); } renderSystemMessage(props) { return ( <SystemMessage {...props} containerStyle={{ marginBottom: 15, }} textStyle={{ fontSize: 14, textAlign: 'center' }} /> ); } render() { return ( <GiftedChat messages={this.state.messages} onSend={message => this.onSend(message)} user={{ _id: parseInt(userId) }} renderBubble={this.renderBubble} renderSystemMessage={this.renderSystemMessage} /> ); } }
Relay Moder - Pagination
I am already working on Pagination. I used PaginationContainer for that. It work’s but no way what I am looking for. I got button next which call props.relay.loadMore(2) function. So when I click on this button it will call query and add me 2 more items to list. It works like load more. But I would like instead of add these two new items to list, replace the old item with new. I try to use this getFragmentVariables for modifying variables for reading from the store but it’s not working. Have somebody Idea or implemented something similar before? class QueuesBookingsList extends Component { props: Props; handleLoadMore = () => { const { hasMore, isLoading, loadMore } = this.props.relay; console.log('hasMore', hasMore()); if (!hasMore() || isLoading()) { return; } this.setState({ isLoading }); loadMore(1, () => { this.setState({ isLoading: false }); }); }; getItems = () => { const edges = idx(this.props, _ => _.data.queuesBookings.edges) || []; return edges.map(edge => edge && edge.node); }; getItemUrl = ({ bid }: { bid: number }) => getDetailUrlWithId(BOOKING, bid); render() { return ( <div> <button onClick={this.handleLoadMore}>TEST</button> <GenericList displayValue={'bid'} items={this.getItems()} itemUrl={this.getItemUrl} emptyText="No matching booking found" /> </div> ); } } export default createPaginationContainer( QueuesBookingsList, { data: graphql` fragment QueuesBookingsList_data on RootQuery { queuesBookings(first: $count, after: $after, queueId: $queueId) #connection( key: "QueuesBookingsList_queuesBookings" filters: ["queueId"] ) { edges { cursor node { id bid url } } pageInfo { endCursor hasNextPage } } } `, }, { direction: 'forward', query: graphql` query QueuesBookingsListQuery( $count: Int! $after: String $queueId: ID ) { ...QueuesBookingsList_data } `, getConnectionFromProps(props) { return props.data && props.data.queuesBookings; }, getFragmentVariables(prevVars, totalCount) { console.log({ prevVars }); return { ...prevVars, count: totalCount, }; }, getVariables(props, variables, fragmentVariables) { return { count: variables.count, after: variables.cursor, queueId: fragmentVariables.queueId, }; }, }, );
As I figure out, there are two solutions, use refechConnection method for Pagination Container or use Refech Container.