2

I am using react-apollo to query the graphQL server and able to successfully hydrate the client with the data. As there will be more than a single place I will be querying for the data I am trying to create a container (refactor) to encapsulate the useQuery hook so that it can be used in one place.

First Try ( working as expected )

const HomeContainer = () => {
  const { data, error, loading } = useQuery(GET_DATA_QUERY, {
    variables: DATA_VARIABLES
  });
  const [transformedData, setTransformedData] = useState();

  useEffect(() => {
    if(!!data) {
      const transformedData = someTransformationFunc(data);

      setTransformedData(...{transformedData});
    }
  }, [data]);

  if (loading) {
    return <div>Loading data ...</div>;
  }

  if (error) {
    return <p>Error loading data</p>;
  }
  if (!data) {
    return <p>Not found</p>;
  }

  return <Home transformedData={transformedData} />;
};

I wanted to encapsulate the ceremony around different stages of the query to a new container ( loading, error state) so that I can reduce code duplication.

First stab at refactoring

  • The Query container gets passed in the query, variables and the callback. This takes the responsibility of returning different nodes based on the state of the query ( loading, error or when no data comes back ).

const HomeContainer = () => {
  const {data, error, loading} = useQuery(GET_DATA_QUERY, {
    variables: DATA_VARIABLES
  });
  const [transformedData, setTransformedData] = useState();

  const callback = (data) => {
    const transformedData = someTransformationFunc(data);

    setTransformedData(...{
      transformedData
    });
  };

  return ( 
     <QueryContainer 
        query={GET_DATA_QUERY}
        variables={DATA_VARIABLES}
        callback ={callback} 
     >
        <Home transformedData={transformedData} />
     </QueryContainer>
  )
};

const QueryContainer = ({callback, query, variables, children }) => {
  const {data, error, loading } = useQuery(query, {
    variables: variables
  });

  // Once data is updated invoke the callback
  // The transformation of the raw data is handled by the child
  useEffect(() => {
    if (!!data) {
      callback(data);
    }
  }, [data]);

  if (loading) {
    return <div > Loading data... < /div>;
  }

  if (error) {
    return <p > Error loading data < /p>;
  }
  if (!data) {
    return <p > Not found < /p>;
  }

  return children;
};

QueryContainer is using useEffect and invokes the callback when data comes back. I felt this is a bit messy and defeats the purpose of encapsulating in the parent and using the callback to talk and update the child.

Third Try ( Using children as function )

Got rid of the callback and passing the data as the first argument to the children function.

const HomeContainer = () => {
    return (
        <QueryContainer
            query={GET_DATA_QUERY}
            variables={DATA_VARIABLES}
        >   
           {(data) => {
               const transformedData = someTransformationFunc(data);

                return <Home transformedData={transformedData} />;
           }}

        </QueryContainer>
    )
};

const QueryContainer = ({ query, variables, children }) => {
    const { data, error, loading } = useQuery(query, {
        variables: variables
    });

    if (loading) {
        return <div>Loading data ...</div>;
    }

    if (error) {
        return <p>Error loading data</p>;
    }
    if (!data) {
        return <p>Not found</p>;
    }

    return children(data);
};

I expected this to work as nothing really changed and the new render when the data is updated calls the children as a function with data as argument. But when I navigate to that route I see a black screen ( no errors and I can see the correct data logged into the console ) If I click the link again I can see the component committed to the DOM.

Not really sure what is going on here and wondering if someone can throw light as to what is going on here.

Sushanth --
  • 55,259
  • 9
  • 66
  • 105

2 Answers2

0

hmmm, should work ...

Try something like this (component injection, a bit like HOC - inspiration) :

const HomeContainer = () => {
  return (
    <QueryContainer
        query={GET_DATA_QUERY}
        variables={DATA_VARIABLES}
        transformation={someTransformationFunc}
        renderedComponent={Home}
    />   
  )
};

const QueryContainer = ({ query, variables, transformation, renderedComponent: View }) => {
  const { data, error, loading } = useQuery(query, { variables });

  if (loading) {
    return <div>Loading data ...</div>;
  }

  if (error) {
    return <p>Error loading data</p>;
  }
  if (!data) {
    return <p>Not found</p>;
  }

  // let transformedData = transformation(data);
  // return <View transformedData={transformedData} />;

  return <View transformedData={transformation ? transformation(data) : data} />;
};

If still not working (!?), pass both data and transformation as props and use them to initialize state (with useState or useEffect).

Do you really/still need <HomeContainer/> as an abstraction? ;)

xadm
  • 8,219
  • 3
  • 14
  • 25
  • `Do you really/still need as an abstraction?` Not really, I was just trying to come up with a minimalistic example. Clubbing `useEffect` or `useState` will definitely work ( the 2nd example uses that ). I am trying to understand as to what might be happening due to which it is not being rendered correctly when using child as a function – Sushanth -- Feb 25 '20 at 00:51
  • I meant 1-liner inside `` `const {someState, setSomeState}= useState(transformation(data));` ... my solution works? you need to know why your doesn't? – xadm Feb 25 '20 at 01:03
  • I have not checked if your solution works or not. But technically the impl is pretty similar to the 3rd one that I have in the question, children as a func vs HOC – Sushanth -- Feb 25 '20 at 01:05
  • Of course, difference can be as small as between working vs not working ;) – xadm Feb 25 '20 at 01:11
  • Found the issue. The implementation is working as expected. Unfortunately the issue was with some other comp that was conditionally rendering a container due to which this did not seen to work. It works with the first 2 implementations die to the fact `setState` was being called. Here is the working example -https://codesandbox.io/s/weathered-currying-4ohh3 – Sushanth -- Feb 25 '20 at 01:51
  • I wrote ... should work ... Then my example should work, too. Choose more readable ;) – xadm Feb 25 '20 at 02:19
  • Sure. I am pretty sure that the one that you have should work after I found the root cause of not being rendered again ( the 3rd impl is working as well after the fix ). I am trying to avoid `HOC` ( it is a great pattern ) but takes a hit with readability as it won't be clear as to what props will be consumed by the comp vs which one will be passed down to the child. Also render prop has more flexibility and a bit more declarative. – Sushanth -- Feb 25 '20 at 21:28
  • It's not a HOC (usually passes all props, injecting new/additional), FaCC is closer ... you can pass `...rest` (`fetchMore`, variables...) ... it's a **matter of opinion/convention** (like graphql passes `data` prop, everyone knows that) ... IMHO FaCC adds more noise (mixing renders with declarations) ... there is a clear view - all defined props are consumed (easy recognizable, `transformation` doesn't need to be separately expressed in render fn) ... did you read the article? – xadm Feb 25 '20 at 22:17
  • Thank you for the explanation. I did read the article and it is helpful. – Sushanth -- Feb 25 '20 at 22:51
0

The code snippets that I have added above is working as expected in isolation.

https://codesandbox.io/s/weathered-currying-4ohh3

The problem was with some other component down the hierarchy tree that was causing the component not to re render.

The 2nd implementation is working as expected as the component is getting rendered again dud to the callback being invoked from the parent.

Sushanth --
  • 55,259
  • 9
  • 66
  • 105