Skip to content

Commit

Permalink
added hooks
Browse files Browse the repository at this point in the history
  • Loading branch information
SamiCharfeddine committed Oct 31, 2019
1 parent 348d688 commit a1322c9
Show file tree
Hide file tree
Showing 8 changed files with 574 additions and 0 deletions.
10 changes: 10 additions & 0 deletions hooks/useEntitySelector.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import { useMemo } from 'react';
import { makeEntitySelector } from 'wappsto-redux/selectors/entities';

const useEntitySelector = (type) => useMemo(() => {
const getEntity = makeEntitySelector();
return (state, options) => getEntity(state, type, options);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

export default useEntitySelector;
98 changes: 98 additions & 0 deletions hooks/useIds.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,98 @@
import { useState, useEffect, useRef, useCallback, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';

import { makeRequest, removeRequest } from 'wappsto-redux/actions/request';
import { setItem } from 'wappsto-redux/actions/items';
import { makeEntitiesSelector } from 'wappsto-redux/selectors/entities';
import { makeItemSelector } from 'wappsto-redux/selectors/items';
import usePrevious from '../hooks/usePrevious';
import useRequest from '../hooks/useRequest';

const itemName = 'useIds_status';
const cache = {};

const setCacheStatus = (dispatch, ids, status) => {
ids.forEach(id => cache[id] = status);
dispatch(setItem(itemName, { ...cache }));
}

function useIds(service, ids, query){
const ownRequest = useRef(false);
const [ status, setStatus ] = useState('idle');
const prevStatus = usePrevious(status);
const missingIds = useRef();
const dispatch = useDispatch();
const [ items, setItems] = useState([]);
const getEntities = useMemo(makeEntitiesSelector, []);
const getItem = useMemo(makeItemSelector, []);
const cacheItems = useSelector(state => getEntities(state, service, { filter: ids.map(id => ({ meta: { id: id } }))}));
const idsStatus = useSelector(state => getItem(state, itemName));

const updateMissingIds = useCallback(() => {
const arr = [];
ids.forEach(id => {
if(cache[id] !== 'pending' && (cache[id] !== 'success' || !cacheItems.find(item => item.meta.id === id))){
arr.push(id);
}
});
missingIds.current = arr;
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);

const { request, requestId, setRequestId } = useRequest();

// Update cache when request is over
useEffect(() => {
if(request && ownRequest.current){
setCacheStatus(dispatch, missingIds.current, request.status);
if(request.status !== 'pending'){
ownRequest.current = false;
}
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, request]);

// Make request to get the ids
useEffect(() => {
updateMissingIds();
if(missingIds.current.length > 0){
setCacheStatus(dispatch, missingIds.current, 'pending');
ownRequest.current = true;
dispatch(removeRequest(requestId));
const lastRequestId = dispatch(makeRequest({
method: 'GET',
url: '/' + service,
query: {
...query,
id: missingIds.current
}
}));
setRequestId(lastRequestId);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [dispatch, service, updateMissingIds]);

// Update status
useEffect(() => {
if(status !== 'success'){
for(let i = 0; i < ids.length; i++){
const idStatus = cache[ids[i]];
if(idStatus === 'error'){
setStatus('error');
return;
} else if(idStatus === 'pending'){
setStatus('pending');
return;
}
}
setStatus('success');
} else if(prevStatus !== 'success' && status === 'success'){
setItems(cacheItems);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ids, idsStatus, status]);

return { items, status };
}

export default useIds;
212 changes: 212 additions & 0 deletions hooks/useList.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,212 @@
import { useState, useEffect, useCallback, useRef, useMemo } from 'react';
import { useSelector, useDispatch } from 'react-redux';

import { setItem } from 'wappsto-redux/actions/items';
import { makeRequest } from 'wappsto-redux/actions/request';

import { makeEntitiesSelector } from 'wappsto-redux/selectors/entities';
import { makeItemSelector } from 'wappsto-redux/selectors/items';
import { getUrlInfo } from 'wappsto-redux/util/helpers';

import usePrevious from '../hooks/usePrevious';
import useRequest from '../hooks/useRequest';

function getQueryObj(query) {
var urlParams = {};
var match,
pl = /\+/g,
search = /([^&=]+)=?([^&]*)/g,
decode = function (s) { return decodeURIComponent(s.replace(pl, " ")); };

while ((match = search.exec(query)))
urlParams[decode(match[1])] = decode(match[2]);
return urlParams;
}

/*
props: url, type, id, childType, query, reset, resetOnEmpty, sort
*/
const empty = [];
function useList(props){
const dispatch = useDispatch();
const prevQuery = usePrevious(props.query);
const query = useRef({});
const differentQuery = useRef(0);
if(JSON.stringify(prevQuery) !== JSON.stringify(props.query)){
differentQuery.current = differentQuery.current + 1;
}

const propsData = useMemo(() => {
let { type, id, childType, url } = props;
let parent, entitiesType;
let query = { ...props.query };
if(url){
let split = url.split("?");
url = split[0];
query = {...getQueryObj(split.slice(1).join("?")), ...query};
split = split[0].split("/");
let result = getUrlInfo(url);
if(result.parent){
type = result.parent.type;
childType = result.service;
entitiesType = childType;
} else {
id = result.id;
type = result.service;
entitiesType = type;
}
} else {
url = "/" + type;
if(id){
if(id.startsWith("?")){
query = { ...query, ...getQueryObj(id.slice(1)) };
} else {
if(!id.startsWith("/")){
url += "/";
}
url += id;
}
}
if(childType){
url += "/" + childType;
parent = { id, type };
entitiesType = childType;
} else {
entitiesType = type;
}
}
return{
type: type,
childType: childType,
entitiesType: entitiesType,
id: id,
url: url,
query: query,
parent: parent
};
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [props.type, props.id, props.childType, props.url, differentQuery.current]);

const { request, setRequestId } = useRequest();
const queryString = JSON.stringify(propsData.query);
const idsItemName = propsData.url + queryString + "_ids";
const fetchedItemName = propsData.url + queryString + "_fetched";
const getSavedIdsItem = useMemo(makeItemSelector, []);
const savedIds = useSelector(state => getSavedIdsItem(state, idsItemName)) || 0;
const getFetchedItem = useMemo(makeItemSelector, []);
const fetched = useSelector(state => getFetchedItem(state, fetchedItemName));

const limit = propsData.query.limit || 100;

const options = { parent: propsData.parent, filter: savedIds };

const getEntities = useMemo(makeEntitiesSelector, []);
let items = useSelector(state => getEntities(state, propsData.entitiesType, options));

if(props.resetOnEmpty){
if(!fetched
&& items.length !== 0
&& (!request
|| request.status === "error"
|| (request.status === "pending" && !request.url.includes("offset") && !request.options.query.hasOwnProperty("offset"))
)
) {
items = empty;
}
}
if(items.length === 1 && items[0].meta.type === 'attributelist'){
const newItems = [];
for(let key in items[0].data){
newItems.push({id: key, [propsData.id]: items[0].data[key]});
}
items = newItems.length > 0 ? newItems : empty;
}

const [ canLoadMore, setCanLoadMore ] = useState(items.length % limit === 0);

useEffect(() => {
items.sort(props.sort);
}, [items, props.sort]);

const prevRequest = usePrevious(request);

const sendRequest = useCallback((options) => {
if(propsData.url){
setCanLoadMore(false);
setRequestId(dispatch(makeRequest("GET", propsData.url, null, options)));
}
}, [dispatch, propsData.url, setRequestId]);

const refresh = useCallback((reset) => {
query.current = {
expand: 0,
...propsData.query
};
sendRequest({
query: query.current,
reset: (typeof reset === "boolean") ? reset : true,
refresh: true
});
}, [propsData.query, sendRequest]);

useEffect(() => {
if((!fetched && (!request || request.status !=='pending')) || (fetched && request && request.status === "error")){
dispatch(setItem(fetchedItemName, true));
refresh(props.reset);
}
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [propsData.query, props.id, propsData.url, refresh, fetched]);

// function updateItemCount
useEffect(() => {
if(prevRequest && prevRequest.status === "pending" && request && request.status === "success"){
dispatch(setItem(idsItemName, (ids) => {
if(request.options.refresh){
ids = [];
}
if(request.json.constructor === Array){
ids = [...ids, ...request.json.map(item => ({ meta: { id: item.meta.id }}))];
} else if(request.json.meta.type === 'attributelist'){
ids = [propsData.id];
}
return ids;
}));
}
}, [dispatch, idsItemName, prevRequest, propsData.id, request]);

// function updateListLoadMore
useEffect(() => {
if(request && prevRequest && prevRequest.status !== "success" && request.status === "success"){
let data;
if(request.json.constructor === Array){
data = request.json;
} else if(request.json.meta.type === 'attributelist'){
data = Object.keys(request.json.data);
} else {
data = [request.json];
}
if(data.length === limit){
setCanLoadMore(true);
} else {
setCanLoadMore(false);
}
}
}, [limit, prevRequest, request]);

const loadMore = useCallback(() => {
if(canLoadMore){
query.current = {
expand: 0,
...propsData.query,
offset: items.length + (propsData.query.offset || 0)
};
sendRequest({
query: query.current
});
}
}, [canLoadMore, propsData.query, items.length, sendRequest]);

return { items, canLoadMore, request, refresh, loadMore };
}

export default useList;
Loading

0 comments on commit a1322c9

Please sign in to comment.